From c57bf78ab7d93fabc1c7e2f70c0ca60dfba750ed Mon Sep 17 00:00:00 2001 From: andykawabata Date: Mon, 24 Jun 2024 14:59:43 -0400 Subject: [PATCH 1/8] allow sorting across all data in history table --- backend/django/core/views/api_annotate.py | 46 ++++---- .../src/components/History/HistoryTable.jsx | 101 +++++++++++++++--- frontend/src/hooks/useHistory.js | 4 +- 3 files changed, 113 insertions(+), 38 deletions(-) diff --git a/backend/django/core/views/api_annotate.py b/backend/django/core/views/api_annotate.py index 897075af..b9afdf29 100644 --- a/backend/django/core/views/api_annotate.py +++ b/backend/django/core/views/api_annotate.py @@ -1030,8 +1030,7 @@ def get_label_history(request, project_pk): current_page = 1 page = int(current_page) - 1 - page_size = 100 - all_data = Data.objects.filter(pk__in=total_data_list).order_by("text") + all_data = Data.objects.filter(pk__in=total_data_list) metadata_objects = MetaDataField.objects.filter(project=project) # filter the results by the search terms @@ -1047,24 +1046,22 @@ def get_label_history(request, project_pk): ).values_list("data__pk", flat=True) all_data = all_data.filter(pk__in=data_with_metadata_filter) - total_pages = math.ceil(len(all_data) / page_size) - - page_data = all_data[page * page_size : min((page + 1) * page_size, len(all_data))] - # get the metadata IDs needed for metadata editing - page_data_metadata_ids = [ - d["metadata"] for d in DataMetadataIDSerializer(page_data, many=True).data + all_data_metadata_ids = [ + d["metadata"] for d in DataMetadataIDSerializer(all_data, many=True).data ] - - page_data = DataSerializer(page_data, many=True).data - + all_data = DataSerializer(all_data, many=True).data # derive the metadata fields in the forms needed for the table - all_metadata = [c.popitem("metadata")[1] for c in page_data] + all_metadata = [c.popitem("metadata")[1] for c in all_data] all_metadata_formatted = [ {c.split(":")[0].replace(" ", "_"): c.split(":")[1] for c in inner_list} for inner_list in all_metadata ] - data_df = pd.DataFrame(page_data).rename(columns={"pk": "id", "text": "data"}) + data_df = pd.DataFrame(all_data).rename(columns={"pk": "id", "text": "data"}) + data_df["metadataIDs"] = all_data_metadata_ids + data_df["metadata"] = all_metadata + data_df["formattedMetadata"] = all_metadata_formatted + if len(data_df) == 0: return Response( { @@ -1129,12 +1126,23 @@ def get_label_history(request, project_pk): # TODO: annotate uses pk while everything else uses ID. Let's fix this data_df["pk"] = data_df["id"] - # now add back on the metadata fields - results = data_df.fillna("").to_dict(orient="records") - for i in range(len(results)): - results[i]["metadata"] = all_metadata[i] - results[i]["formattedMetadata"] = all_metadata_formatted[i] - results[i]["metadataIDs"] = page_data_metadata_ids[i] + sort_by = request.GET.get("sort-by") + reverse = request.GET.get("reverse") + if sort_by is not None: + if data_df[sort_by].dtype == "object": # check if the column is of string type + df_sorted = data_df.sort_values(by=sort_by, key=lambda col: col.str.lower(), ascending=(reverse == "false")) + else: + df_sorted = data_df.sort_values(by=sort_by, ascending=(reverse == "false")) + else: + df_sorted = data_df.sort_values(by="data", key=lambda col: col.str.lower(), ascending=(reverse == "false")) + + # Paginate after sorting + page_size = 100 + total_pages = math.ceil(len(all_data) / page_size) + + page_data = df_sorted.iloc[page * page_size : min((page + 1) * page_size, len(data_df))] + + results = page_data.fillna("").to_dict(orient="records") return Response( { diff --git a/frontend/src/components/History/HistoryTable.jsx b/frontend/src/components/History/HistoryTable.jsx index 9803bd2b..92ebf9b7 100644 --- a/frontend/src/components/History/HistoryTable.jsx +++ b/frontend/src/components/History/HistoryTable.jsx @@ -10,9 +10,8 @@ import { import React, { Fragment, useState, useEffect } from "react"; import { Button, Form, OverlayTrigger, Table, Tooltip } from "react-bootstrap"; -import { DebouncedInput, GrayBox, H4 } from "../ui"; +import { GrayBox, H4 } from "../ui"; import DataCard, { PAGES } from "../DataCard/DataCard"; -import FilterForm from "./FilterForm"; import { useHistory, useVerifyLabel } from "../../hooks"; import { PROJECT_USES_IRR } from "../../store"; @@ -88,12 +87,24 @@ const HistoryTable = () => { const [page, setPage] = useState(0); const [unlabeled, setUnlabeled] = useState(false); const [filters, setFilters] = useState({ Text: "" }); + const [sortBy, setSortBy] = useState("data"); + const [reverseSort, setReverseSort] = useState(false); const [filtersInitialized, setFiltersInitialized] = useState(false); const [shouldRefetch, setShouldRefetch] = useState(false); - const { data: historyData, refetch: refetchHistory } = useHistory(page + 1, unlabeled, filters); + const { data: historyData, refetch: refetchHistory } = useHistory(page + 1, unlabeled, filters, sortBy, reverseSort); const { mutate: verifyLabel } = useVerifyLabel(); + const sortOptions = { + data: "Data", + label: "Label", + profile: "Labeled By", + timestamp: "Timestamp", + verified: "Verified", + verified_by: "Verified By", + pre_loaded: "Pre-Loaded", + }; + const metadataColumnsAccessorKeys = []; if (historyData) { historyData.data.forEach((data) => { @@ -115,12 +126,8 @@ const HistoryTable = () => { const metadataFields = historyData.metadata_fields || []; const filterDefault = getFilterDefault(metadataFields); setFilters(filterDefault); - setShouldRefetch(true); - }; - - const handleApplyFilter = (event) => { - event.preventDefault(); - setPage(0); + setSortBy("data"); + setReverseSort(false); setShouldRefetch(true); }; @@ -132,6 +139,17 @@ const HistoryTable = () => { })); }; + const handleSortInputChange = (event) => { + const { value } = event.target; + setSortBy(value); + }; + + function handleApply(event) { + event.preventDefault(); + setPage(0); + setShouldRefetch(true); + } + useEffect(() => { // initialize filters if (historyData && !filtersInitialized) { const metadataFields = historyData.metadata_fields || []; @@ -243,15 +261,64 @@ const HistoryTable = () => { )} -
+
-

Filters

- < FilterForm - filters={filters} - handleInputChange={handleFilterInputChange} - resetFilters={resetFilters} - handleSubmit={handleApplyFilter} - /> +
+

Filters

+
+ + +
+ {Object.keys(filters).map(field => { + return field !== "Text" ? ( +
+ + +
+ ) : null; + })} +

Sorting

+
+ + +
+
+ setReverseSort(!reverseSort)} + /> + +
+ + +
diff --git a/frontend/src/hooks/useHistory.js b/frontend/src/hooks/useHistory.js index 83688850..9477663a 100644 --- a/frontend/src/hooks/useHistory.js +++ b/frontend/src/hooks/useHistory.js @@ -3,11 +3,11 @@ import { useQuery } from "@tanstack/react-query"; import { PROJECT_ID } from "../store"; import { getConfig } from "../utils/fetch_configs"; -const useHistory = (page, unlabeled, filters) => +const useHistory = (page, unlabeled, filters, sortBy, reverse) => useQuery({ queryKey: ["history", PROJECT_ID, page, unlabeled], queryFn: () => - fetch(`/api/get_label_history/${PROJECT_ID}/?${new URLSearchParams({ page, unlabeled, ...filters }).toString()}`, getConfig) + fetch(`/api/get_label_history/${PROJECT_ID}/?${new URLSearchParams({ page, unlabeled, ...filters, "sort-by": sortBy, reverse }).toString()}`, getConfig) .then((res) => res.json()) }); From f964edecc0f81fd76118e3c06f79f8a63fcf5ede Mon Sep 17 00:00:00 2001 From: andykawabata Date: Mon, 24 Jun 2024 16:40:44 -0400 Subject: [PATCH 2/8] add reverse sort for data --- backend/django/core/views/api_annotate.py | 34 ++++++++++--------- .../src/components/History/HistoryTable.jsx | 1 + 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/backend/django/core/views/api_annotate.py b/backend/django/core/views/api_annotate.py index b9afdf29..1698fa54 100644 --- a/backend/django/core/views/api_annotate.py +++ b/backend/django/core/views/api_annotate.py @@ -1030,14 +1030,26 @@ def get_label_history(request, project_pk): current_page = 1 page = int(current_page) - 1 - all_data = Data.objects.filter(pk__in=total_data_list) + reverse = request.GET.get("reverse", "false").lower() == "true" + order_field = "-text" if reverse else "text" + all_data = Data.objects.filter(pk__in=total_data_list).order_by(order_field) metadata_objects = MetaDataField.objects.filter(project=project) - + # filter the results by the search terms text_filter = request.GET.get("Text") if text_filter is not None and text_filter != "": all_data = all_data.filter(text__icontains=text_filter) + page_size = 100 + total_pages = math.ceil(len(all_data) / page_size) + sort_by = request.GET.get("sort-by") + pre_sorted = False + if sort_by == "data": + # if sorting by data text we can sort and filter early + # otherwise we must wait until other columns are joined to sort and filter + pre_sorted = True + all_data = all_data[page * page_size : min((page + 1) * page_size, len(all_data))] + for m in metadata_objects: m_filter = request.GET.get(str(m)) if m_filter is not None and m_filter != "": @@ -1126,21 +1138,11 @@ def get_label_history(request, project_pk): # TODO: annotate uses pk while everything else uses ID. Let's fix this data_df["pk"] = data_df["id"] - sort_by = request.GET.get("sort-by") - reverse = request.GET.get("reverse") - if sort_by is not None: - if data_df[sort_by].dtype == "object": # check if the column is of string type - df_sorted = data_df.sort_values(by=sort_by, key=lambda col: col.str.lower(), ascending=(reverse == "false")) - else: - df_sorted = data_df.sort_values(by=sort_by, ascending=(reverse == "false")) + if pre_sorted: + page_data = data_df else: - df_sorted = data_df.sort_values(by="data", key=lambda col: col.str.lower(), ascending=(reverse == "false")) - - # Paginate after sorting - page_size = 100 - total_pages = math.ceil(len(all_data) / page_size) - - page_data = df_sorted.iloc[page * page_size : min((page + 1) * page_size, len(data_df))] + df_sorted = data_df.sort_values(by=[sort_by, "data"], key=lambda col: col.str.lower(), ascending=(not reverse)) + page_data = df_sorted.iloc[page * page_size : min((page + 1) * page_size, len(data_df))] results = page_data.fillna("").to_dict(orient="records") diff --git a/frontend/src/components/History/HistoryTable.jsx b/frontend/src/components/History/HistoryTable.jsx index 92ebf9b7..9c071e7d 100644 --- a/frontend/src/components/History/HistoryTable.jsx +++ b/frontend/src/components/History/HistoryTable.jsx @@ -128,6 +128,7 @@ const HistoryTable = () => { setFilters(filterDefault); setSortBy("data"); setReverseSort(false); + setPage(0); setShouldRefetch(true); }; From feab3c94b4b4ec2e419f5ca3e319447d3c7338a1 Mon Sep 17 00:00:00 2001 From: andykawabata Date: Mon, 24 Jun 2024 16:48:03 -0400 Subject: [PATCH 3/8] clean up --- backend/django/core/views/api_annotate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/django/core/views/api_annotate.py b/backend/django/core/views/api_annotate.py index 1698fa54..c7eecc0e 100644 --- a/backend/django/core/views/api_annotate.py +++ b/backend/django/core/views/api_annotate.py @@ -1033,7 +1033,6 @@ def get_label_history(request, project_pk): reverse = request.GET.get("reverse", "false").lower() == "true" order_field = "-text" if reverse else "text" all_data = Data.objects.filter(pk__in=total_data_list).order_by(order_field) - metadata_objects = MetaDataField.objects.filter(project=project) # filter the results by the search terms text_filter = request.GET.get("Text") @@ -1045,11 +1044,12 @@ def get_label_history(request, project_pk): sort_by = request.GET.get("sort-by") pre_sorted = False if sort_by == "data": - # if sorting by data text we can sort and filter early - # otherwise we must wait until other columns are joined to sort and filter + # if sorting by data text we can sort and paginate early + # otherwise we must wait until other columns are joined to sort and paginate pre_sorted = True all_data = all_data[page * page_size : min((page + 1) * page_size, len(all_data))] + metadata_objects = MetaDataField.objects.filter(project=project) for m in metadata_objects: m_filter = request.GET.get(str(m)) if m_filter is not None and m_filter != "": From 034f5883d56ad549104c5d341f06e67e53513f98 Mon Sep 17 00:00:00 2001 From: andykawabata Date: Wed, 26 Jun 2024 16:01:52 -0400 Subject: [PATCH 4/8] Allow admins to view user labels in IRR tab --- backend/django/core/urls/api.py | 1 + backend/django/core/views/api_admin.py | 28 +++++++++++++++++ frontend/src/actions/adminTables.js | 25 +++++++++++++++ .../src/components/AdminTable/IRRtable.jsx | 31 +++++++++++++++++++ frontend/src/components/AdminTable/index.jsx | 29 ++++++++++++----- .../src/containers/adminTable_container.jsx | 6 +++- frontend/src/reducers/adminTables.js | 8 +++-- frontend/src/styles/smart.scss | 18 +++++++++++ 8 files changed, 136 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/AdminTable/IRRtable.jsx diff --git a/backend/django/core/urls/api.py b/backend/django/core/urls/api.py index 516be63f..14f7866c 100644 --- a/backend/django/core/urls/api.py +++ b/backend/django/core/urls/api.py @@ -85,6 +85,7 @@ re_path(r"^get_irr_metrics/(?P\d+)/$", api_admin.get_irr_metrics), re_path(r"^heat_map_data/(?P\d+)/$", api_admin.heat_map_data), re_path(r"^perc_agree_table/(?P\d+)/$", api_admin.perc_agree_table), + re_path(r"^irr_log/(?P\d+)/$", api_admin.irr_log), re_path(r"^project_status/(?P\d+)/$", api_admin.get_project_status), re_path( r"^unassign_coder/(?P\d+)/(?P\d+)/$", diff --git a/backend/django/core/views/api_admin.py b/backend/django/core/views/api_admin.py index 7863e986..f51c5e97 100644 --- a/backend/django/core/views/api_admin.py +++ b/backend/django/core/views/api_admin.py @@ -286,6 +286,34 @@ def perc_agree_table(request, project_pk): user_agree = perc_agreement_table_data(project) return Response({"data": user_agree}) +@api_view(["GET"]) +@permission_classes((IsAdminOrCreator,)) +def irr_log(request, project_pk): + """Gets IRR user labels for a project. + Only gets IRR logs with label disagreements i.e. data in the admin queue.""" + project = Project.objects.get(pk=project_pk) + + irr_logs = IRRLog.objects.filter( + data__project=project, + data__queues__type='admin' + ) + + data_labels = {} + + for log in irr_logs: + data_id = log.data_id + username = log.profile.user.username + label = log.label.name + + if data_id not in data_labels: + data_labels[data_id] = {"data_id": data_id} + data_labels[data_id][username] = label + + response_data = list(data_labels.values()) + + return Response({"data": response_data}) + + @api_view(["GET"]) @permission_classes((IsAdminOrCreator,)) diff --git a/frontend/src/actions/adminTables.js b/frontend/src/actions/adminTables.js index b93a0f43..c20b54de 100644 --- a/frontend/src/actions/adminTables.js +++ b/frontend/src/actions/adminTables.js @@ -11,10 +11,12 @@ import { queryClient } from "../store"; export const SET_ADMIN_DATA = 'SET_ADMIN_DATA'; export const SET_DISCARDED_DATA = 'SET_DISCARDED_DATA'; export const SET_ADMIN_COUNTS = 'SET_ADMIN_COUNTS'; +export const SET_IRR_LOG = 'SET_IRR_LOG'; export const set_admin_data = createAction(SET_ADMIN_DATA); export const set_discarded_data = createAction(SET_DISCARDED_DATA); export const set_admin_counts = createAction(SET_ADMIN_COUNTS); +export const set_irr_log = createAction(SET_IRR_LOG); //get the skipped data for the admin Table @@ -52,6 +54,29 @@ export const getAdmin = (projectID) => { }; }; +export const getIrrLog = (projectID) => { + let apiURL = `/api/irr_log/${projectID}/`; + return dispatch => { + return fetch(apiURL, getConfig()) + .then(response => { + if (response.ok) { + return response.json(); + } else { + const error = new Error(response.statusText); + error.response = response; + throw error; + } + }) + .then(response => { + if ('error' in response) { + return dispatch(setMessage(response.error)); + } else { + dispatch(set_irr_log(response.data)); + } + }); + }; +}; + export const adminLabel = (dataID, labelID, projectID) => { let payload = { labelID: labelID, diff --git a/frontend/src/components/AdminTable/IRRtable.jsx b/frontend/src/components/AdminTable/IRRtable.jsx new file mode 100644 index 00000000..d945d83e --- /dev/null +++ b/frontend/src/components/AdminTable/IRRtable.jsx @@ -0,0 +1,31 @@ +import React, { Fragment } from "react"; +import { Card, Table } from "react-bootstrap"; + +const IRRtable = ({ irrEntry }) => { + if (!irrEntry || !Object.keys(irrEntry).length){ + return Error loading IRR data; + } + const { data_id, ...irrData } = irrEntry; + return ( + + + + + + + + + + {Object.entries(irrData).map(([user, label], index) => ( + + + + + ))} + +
UserLabel
{user}{label}
+
+ ); +}; + +export default IRRtable; diff --git a/frontend/src/components/AdminTable/index.jsx b/frontend/src/components/AdminTable/index.jsx index c3e523be..66ebe326 100644 --- a/frontend/src/components/AdminTable/index.jsx +++ b/frontend/src/components/AdminTable/index.jsx @@ -3,10 +3,12 @@ import PropTypes from "prop-types"; import ReactTable from "react-table-6"; import CodebookLabelMenuContainer from "../../containers/codebookLabelMenu_container"; import DataCard, { PAGES } from "../DataCard/DataCard"; +import IRRtable from "./IRRtable"; class AdminTable extends React.Component { componentDidMount() { this.props.getAdmin(); + this.props.getIrrLog(); } getText(row) { @@ -26,7 +28,13 @@ class AdminTable extends React.Component { } render() { - const { admin_data, labels, message, adminLabel, discardData } = this.props; + const { admin_data, irr_log, labels, message, adminLabel, discardData } = this.props; + + const getIrrEntry = data_id => { + const irr_entry = irr_log.find(entry => entry.data_id === data_id); + if (irr_entry) return irr_entry; + return {}; + }; const columns = [ { @@ -57,15 +65,22 @@ class AdminTable extends React.Component {

{row.original.message}

)} - +
+ + { row.original.reason === "IRR" && + + } +
); } - } + }, + // column for coder, label table + ]; let page_sizes = [1]; diff --git a/frontend/src/containers/adminTable_container.jsx b/frontend/src/containers/adminTable_container.jsx index 5950c237..1facbf5b 100644 --- a/frontend/src/containers/adminTable_container.jsx +++ b/frontend/src/containers/adminTable_container.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; -import { adminLabel, getAdmin, discardData } from '../actions/adminTables'; +import { adminLabel, getAdmin, getIrrLog, discardData } from '../actions/adminTables'; import AdminTable from '../components/AdminTable'; const PROJECT_ID = window.PROJECT_ID; @@ -11,6 +11,7 @@ const AdminTableContainer = (props) => ; const mapStateToProps = (state) => { return { admin_data: state.adminTables.admin_data, + irr_log: state.adminTables.irr_log, labels: state.smart.labels, message: state.card.message, admin_counts: state.adminTables.admin_counts @@ -25,6 +26,9 @@ const mapDispatchToProps = (dispatch) => { getAdmin: () => { dispatch(getAdmin(PROJECT_ID)); }, + getIrrLog: () => { + dispatch(getIrrLog(PROJECT_ID)); + }, discardData: (dataID) => { dispatch(discardData(dataID, PROJECT_ID)); } diff --git a/frontend/src/reducers/adminTables.js b/frontend/src/reducers/adminTables.js index 92188466..de8bf813 100644 --- a/frontend/src/reducers/adminTables.js +++ b/frontend/src/reducers/adminTables.js @@ -1,17 +1,21 @@ import { handleActions } from 'redux-actions'; import update from 'immutability-helper'; -import { SET_ADMIN_DATA, SET_ADMIN_COUNTS } from '../actions/adminTables'; +import { SET_ADMIN_DATA, SET_ADMIN_COUNTS, SET_IRR_LOG } from '../actions/adminTables'; const initialState = { admin_data: [], - admin_counts: [] + admin_counts: [], + irr_log: [] }; const adminTables = handleActions({ [SET_ADMIN_DATA]: (state, action) => { return update(state, { admin_data: { $set: action.payload } } ); }, + [SET_IRR_LOG]: (state, action) => { + return update(state, { irr_log: { $set: action.payload } } ); + }, [SET_ADMIN_COUNTS]: (state, action) => { return update(state, { admin_counts: { $set: action.payload } } ); } diff --git a/frontend/src/styles/smart.scss b/frontend/src/styles/smart.scss index 94d84381..0cd17d25 100644 --- a/frontend/src/styles/smart.scss +++ b/frontend/src/styles/smart.scss @@ -751,3 +751,21 @@ li.disabled { margin-right: 5px; } } + +.admin-data-card-wrapper { + display: flex; + + > :first-child { + flex-grow: 1; + display: block !important; + } + + .irr-card { + width: 425px; + overflow-x: scroll + } + + .btn-toolbar { + display: block; + } +} \ No newline at end of file From bca01c6b9fbe80827df8792a60afc8f61fac0c86 Mon Sep 17 00:00:00 2001 From: andykawabata Date: Thu, 27 Jun 2024 15:19:03 -0400 Subject: [PATCH 5/8] add description --- backend/django/core/views/api_admin.py | 4 ++-- frontend/src/components/AdminTable/IRRtable.jsx | 7 ++++--- frontend/src/components/AdminTable/index.jsx | 16 +++++++++++++--- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/backend/django/core/views/api_admin.py b/backend/django/core/views/api_admin.py index f51c5e97..979d5492 100644 --- a/backend/django/core/views/api_admin.py +++ b/backend/django/core/views/api_admin.py @@ -303,11 +303,11 @@ def irr_log(request, project_pk): for log in irr_logs: data_id = log.data_id username = log.profile.user.username - label = log.label.name + label_id = log.label_id if data_id not in data_labels: data_labels[data_id] = {"data_id": data_id} - data_labels[data_id][username] = label + data_labels[data_id][username] = label_id response_data = list(data_labels.values()) diff --git a/frontend/src/components/AdminTable/IRRtable.jsx b/frontend/src/components/AdminTable/IRRtable.jsx index d945d83e..b37f79a4 100644 --- a/frontend/src/components/AdminTable/IRRtable.jsx +++ b/frontend/src/components/AdminTable/IRRtable.jsx @@ -5,7 +5,6 @@ const IRRtable = ({ irrEntry }) => { if (!irrEntry || !Object.keys(irrEntry).length){ return Error loading IRR data; } - const { data_id, ...irrData } = irrEntry; return ( @@ -13,13 +12,15 @@ const IRRtable = ({ irrEntry }) => { + - {Object.entries(irrData).map(([user, label], index) => ( + {Object.entries(irrEntry).map(([user, label], index) => ( - + + ))} diff --git a/frontend/src/components/AdminTable/index.jsx b/frontend/src/components/AdminTable/index.jsx index 66ebe326..d7f89785 100644 --- a/frontend/src/components/AdminTable/index.jsx +++ b/frontend/src/components/AdminTable/index.jsx @@ -32,8 +32,18 @@ class AdminTable extends React.Component { const getIrrEntry = data_id => { const irr_entry = irr_log.find(entry => entry.data_id === data_id); - if (irr_entry) return irr_entry; - return {}; + const irr_entry_formatted = {}; + for (let user in irr_entry) { + if (user === "data_id") continue; + const label_id = irr_entry[user]; + if (!label_id) { + // situation where the irr data was adjudicated instead of labeled + irr_entry_formatted[user] = { name: "", description: "" }; + } else { + irr_entry_formatted[user] = labels.find(label => label.pk === label_id); + } + } + return irr_entry_formatted; }; const columns = [ @@ -71,7 +81,7 @@ class AdminTable extends React.Component { page={PAGES.ADMIN} actions={{ onSelectLabel: adminLabel, onDiscard: discardData }} /> - { row.original.reason === "IRR" && + { row.original.reason === "IRR" && irr_log.length && } From 19b4e3e82f89d94ffad7dc7d32e3f6ee19c668a7 Mon Sep 17 00:00:00 2001 From: andykawabata Date: Thu, 27 Jun 2024 16:59:00 -0400 Subject: [PATCH 6/8] increase width of table --- frontend/src/styles/smart.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/styles/smart.scss b/frontend/src/styles/smart.scss index 0cd17d25..ea2752d5 100644 --- a/frontend/src/styles/smart.scss +++ b/frontend/src/styles/smart.scss @@ -761,7 +761,7 @@ li.disabled { } .irr-card { - width: 425px; + width: 450px; overflow-x: scroll } From 451d463384de0bd8c1888b8d0f32459f7a473a77 Mon Sep 17 00:00:00 2001 From: andykawabata Date: Mon, 1 Jul 2024 16:01:14 -0400 Subject: [PATCH 7/8] Improve sorting performance --- backend/django/core/views/api_annotate.py | 62 ++++++++++++----------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/backend/django/core/views/api_annotate.py b/backend/django/core/views/api_annotate.py index c7eecc0e..99bff0f3 100644 --- a/backend/django/core/views/api_annotate.py +++ b/backend/django/core/views/api_annotate.py @@ -1030,25 +1030,30 @@ def get_label_history(request, project_pk): current_page = 1 page = int(current_page) - 1 + sort_by = request.GET.get("sort-by") reverse = request.GET.get("reverse", "false").lower() == "true" - order_field = "-text" if reverse else "text" - all_data = Data.objects.filter(pk__in=total_data_list).order_by(order_field) + sort_options = { + 'data': 'text', + 'label': 'datalabel__label__name', + 'profile': 'datalabel__profile__user__username', + 'timestamp': 'datalabel__timestamp', + 'verified': 'datalabel__verified__pk', + 'verified_by': 'datalabel__verified__verified_by__user__username', + 'pre_loaded': 'datalabel__pre_loaded' + } + order_field = sort_options.get(sort_by, 'text') + + if reverse: + order_field = '-' + order_field + + all_data = Data.objects.filter(pk__in=total_data_list).order_by(order_field) + # filter the results by the search terms text_filter = request.GET.get("Text") if text_filter is not None and text_filter != "": all_data = all_data.filter(text__icontains=text_filter) - page_size = 100 - total_pages = math.ceil(len(all_data) / page_size) - sort_by = request.GET.get("sort-by") - pre_sorted = False - if sort_by == "data": - # if sorting by data text we can sort and paginate early - # otherwise we must wait until other columns are joined to sort and paginate - pre_sorted = True - all_data = all_data[page * page_size : min((page + 1) * page_size, len(all_data))] - metadata_objects = MetaDataField.objects.filter(project=project) for m in metadata_objects: m_filter = request.GET.get(str(m)) @@ -1058,21 +1063,26 @@ def get_label_history(request, project_pk): ).values_list("data__pk", flat=True) all_data = all_data.filter(pk__in=data_with_metadata_filter) - all_data_metadata_ids = [ - d["metadata"] for d in DataMetadataIDSerializer(all_data, many=True).data + page_size = 100 + total_pages = math.ceil(len(all_data) / page_size) + pre_sorted = False + page_data = all_data[page * page_size : min((page + 1) * page_size, len(all_data))] + + page_data_metadata_ids = [ + d["metadata"] for d in DataMetadataIDSerializer(page_data, many=True).data ] - all_data = DataSerializer(all_data, many=True).data + page_data = DataSerializer(page_data, many=True).data # derive the metadata fields in the forms needed for the table - all_metadata = [c.popitem("metadata")[1] for c in all_data] - all_metadata_formatted = [ + page_metadata = [c.popitem("metadata")[1] for c in page_data] + page_metadata_formatted = [ {c.split(":")[0].replace(" ", "_"): c.split(":")[1] for c in inner_list} - for inner_list in all_metadata + for inner_list in page_metadata ] - data_df = pd.DataFrame(all_data).rename(columns={"pk": "id", "text": "data"}) - data_df["metadataIDs"] = all_data_metadata_ids - data_df["metadata"] = all_metadata - data_df["formattedMetadata"] = all_metadata_formatted + data_df = pd.DataFrame(page_data).rename(columns={"pk": "id", "text": "data"}) + data_df["metadataIDs"] = page_data_metadata_ids + data_df["metadata"] = page_metadata + data_df["formattedMetadata"] = page_metadata_formatted if len(data_df) == 0: return Response( @@ -1138,13 +1148,7 @@ def get_label_history(request, project_pk): # TODO: annotate uses pk while everything else uses ID. Let's fix this data_df["pk"] = data_df["id"] - if pre_sorted: - page_data = data_df - else: - df_sorted = data_df.sort_values(by=[sort_by, "data"], key=lambda col: col.str.lower(), ascending=(not reverse)) - page_data = df_sorted.iloc[page * page_size : min((page + 1) * page_size, len(data_df))] - - results = page_data.fillna("").to_dict(orient="records") + results = data_df.fillna("").to_dict(orient="records") return Response( { From 93f49f795e35307896921df34482faa43b722c4b Mon Sep 17 00:00:00 2001 From: andykawabata Date: Wed, 3 Jul 2024 10:57:01 -0400 Subject: [PATCH 8/8] make endpoint more generic --- backend/django/core/views/api_admin.py | 29 +++++++------------ frontend/src/actions/adminTables.js | 7 +++-- frontend/src/components/AdminTable/index.jsx | 12 ++++---- .../src/containers/adminTable_container.jsx | 2 +- frontend/src/styles/smart.scss | 4 +++ 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/backend/django/core/views/api_admin.py b/backend/django/core/views/api_admin.py index 979d5492..f574a368 100644 --- a/backend/django/core/views/api_admin.py +++ b/backend/django/core/views/api_admin.py @@ -5,6 +5,7 @@ from postgres_stats.aggregates import Percentile from rest_framework.decorators import api_view, permission_classes from rest_framework.response import Response +from core.serializers import IRRLogModelSerializer from core.models import ( AssignedData, @@ -289,29 +290,21 @@ def perc_agree_table(request, project_pk): @api_view(["GET"]) @permission_classes((IsAdminOrCreator,)) def irr_log(request, project_pk): - """Gets IRR user labels for a project. - Only gets IRR logs with label disagreements i.e. data in the admin queue.""" + """ + Gets IRR user labels for a project. Optionally filters to include only + logs with label disagreements (i.e., data in the admin queue) based on a query parameter. + """ project = Project.objects.get(pk=project_pk) - irr_logs = IRRLog.objects.filter( - data__project=project, - data__queues__type='admin' - ) - - data_labels = {} - - for log in irr_logs: - data_id = log.data_id - username = log.profile.user.username - label_id = log.label_id + admin_queue_only = request.query_params.get('admin', 'false').lower() == 'true' - if data_id not in data_labels: - data_labels[data_id] = {"data_id": data_id} - data_labels[data_id][username] = label_id + irr_log = IRRLog.objects.filter(data__project=project) + if admin_queue_only: + irr_log = irr_log.filter(data__queues__type='admin') - response_data = list(data_labels.values()) + irr_log_serialized = IRRLogModelSerializer(irr_log, many=True).data - return Response({"data": response_data}) + return Response({"irr_log": irr_log_serialized}) diff --git a/frontend/src/actions/adminTables.js b/frontend/src/actions/adminTables.js index c20b54de..f5ec30c9 100644 --- a/frontend/src/actions/adminTables.js +++ b/frontend/src/actions/adminTables.js @@ -54,8 +54,11 @@ export const getAdmin = (projectID) => { }; }; -export const getIrrLog = (projectID) => { +export const getIrrLog = (projectID, adminOnly = false) => { let apiURL = `/api/irr_log/${projectID}/`; + + if (adminOnly) apiURL += '?admin=true'; + return dispatch => { return fetch(apiURL, getConfig()) .then(response => { @@ -71,7 +74,7 @@ export const getIrrLog = (projectID) => { if ('error' in response) { return dispatch(setMessage(response.error)); } else { - dispatch(set_irr_log(response.data)); + dispatch(set_irr_log(response.irr_log)); } }); }; diff --git a/frontend/src/components/AdminTable/index.jsx b/frontend/src/components/AdminTable/index.jsx index d7f89785..872db43c 100644 --- a/frontend/src/components/AdminTable/index.jsx +++ b/frontend/src/components/AdminTable/index.jsx @@ -31,16 +31,16 @@ class AdminTable extends React.Component { const { admin_data, irr_log, labels, message, adminLabel, discardData } = this.props; const getIrrEntry = data_id => { - const irr_entry = irr_log.find(entry => entry.data_id === data_id); + const relevant_irr_entries = irr_log.filter(entry => entry.data === data_id); const irr_entry_formatted = {}; - for (let user in irr_entry) { - if (user === "data_id") continue; - const label_id = irr_entry[user]; + for (let entry of relevant_irr_entries) { + const username = entry.profile; + const label_id = entry.label; if (!label_id) { // situation where the irr data was adjudicated instead of labeled - irr_entry_formatted[user] = { name: "", description: "" }; + irr_entry_formatted[username] = { name: "", description: "" }; } else { - irr_entry_formatted[user] = labels.find(label => label.pk === label_id); + irr_entry_formatted[username] = labels.find(label => label.pk === label_id); } } return irr_entry_formatted; diff --git a/frontend/src/containers/adminTable_container.jsx b/frontend/src/containers/adminTable_container.jsx index 1facbf5b..51f57e51 100644 --- a/frontend/src/containers/adminTable_container.jsx +++ b/frontend/src/containers/adminTable_container.jsx @@ -27,7 +27,7 @@ const mapDispatchToProps = (dispatch) => { dispatch(getAdmin(PROJECT_ID)); }, getIrrLog: () => { - dispatch(getIrrLog(PROJECT_ID)); + dispatch(getIrrLog(PROJECT_ID, true)); }, discardData: (dataID) => { dispatch(discardData(dataID, PROJECT_ID)); diff --git a/frontend/src/styles/smart.scss b/frontend/src/styles/smart.scss index ea2752d5..f86b87ed 100644 --- a/frontend/src/styles/smart.scss +++ b/frontend/src/styles/smart.scss @@ -758,6 +758,10 @@ li.disabled { > :first-child { flex-grow: 1; display: block !important; + + p { + max-width: 500px; + } } .irr-card {
User LabelDescription
{user}{label}{label.name}{label.description}