diff --git a/backend/django/core/serializers.py b/backend/django/core/serializers.py index 5814757e..c9716c58 100644 --- a/backend/django/core/serializers.py +++ b/backend/django/core/serializers.py @@ -102,13 +102,21 @@ class Meta: class IRRLogModelSerializer(serializers.ModelSerializer): profile = serializers.StringRelatedField(many=False, read_only=True) + label_name = serializers.SerializerMethodField() + label_description = serializers.SerializerMethodField() timestamp = serializers.DateTimeField( default_timezone=pytz.timezone(TIME_ZONE_FRONTEND), format="%Y-%m-%d, %I:%M %p" ) class Meta: model = IRRLog - fields = ("data", "profile", "label", "timestamp") + fields = ("data", "profile", "label", "label_name", "label_description", "timestamp") + + def get_label_name(self, obj): + return obj.label.name if obj.label else None + + def get_label_description(self, obj): + return obj.label.description if obj.label else None class IRRLog(serializers.HyperlinkedModelSerializer): diff --git a/backend/django/core/templates/projects/detail.html b/backend/django/core/templates/projects/detail.html index fa134b86..39179cd0 100644 --- a/backend/django/core/templates/projects/detail.html +++ b/backend/django/core/templates/projects/detail.html @@ -285,6 +285,9 @@
{% endif %} {% endif %} {% endif %} + {% if project.percentage_irr > 0 %} + + {% endif %} @@ -346,6 +349,32 @@
xhttp.send(); } +/* + * When the download IRR log button is pressed, download the IRR log as a csv file + */ +function downloadIRRLog(projectId) { + var url = `/api/download_irr_log/${projectId}/`; + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (xhttp.readyState === 4 && xhttp.status === 200) { + var blob = new Blob([xhttp.response], {type: 'text/csv'}); + var downloadUrl = window.URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = downloadUrl; + a.download = 'irr_log_' + projectId + '.csv'; + document.body.appendChild(a); + a.click(); + a.remove(); + window.URL.revokeObjectURL(downloadUrl); + } else if (xhttp.readyState === 4 && xhttp.status !== 200) { + console.error('Error downloading the file:', xhttp.statusText); + } + }; + xhttp.open('GET', url, true); + xhttp.responseType = 'blob'; // Set the response type to blob for binary data + xhttp.send(); +} + /* * When the ingest datatable button is pressed, SMART pulls the entire @@ -396,5 +425,7 @@
} }) }); + + {% endblock %} diff --git a/backend/django/core/templates/smart/smart.html b/backend/django/core/templates/smart/smart.html index 9c84f3be..aa2188de 100644 --- a/backend/django/core/templates/smart/smart.html +++ b/backend/django/core/templates/smart/smart.html @@ -16,6 +16,7 @@ {% endif %} window.ADMIN = {{admin}}; window.PROJECT_USES_IRR = {{project_uses_irr}}; + window.PROJECT_SUGGESTION_MAX = {{project_suggestion_max}}; window.onload = function (e) { $.ajax({ diff --git a/backend/django/core/urls/api.py b/backend/django/core/urls/api.py index 04b4ab77..3f1cb436 100644 --- a/backend/django/core/urls/api.py +++ b/backend/django/core/urls/api.py @@ -87,6 +87,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+)/$", @@ -100,6 +101,7 @@ re_path( r"^download_data/(?P\d+)/(?P\d)/$", api.download_data ), + re_path(r"^download_irr_log/(?P\d+)/$", api.download_irr_log), re_path( r"^download_model/(?P\d+)/(?P\d)/$", api.download_model ), diff --git a/backend/django/core/views/api.py b/backend/django/core/views/api.py index 7faec5da..90bb69cf 100644 --- a/backend/django/core/views/api.py +++ b/backend/django/core/views/api.py @@ -10,7 +10,7 @@ from rest_framework.decorators import api_view, permission_classes from rest_framework.response import Response -from core.models import Project +from core.models import Project, IRRLog from core.permissions import IsAdminOrCreator from core.templatetags import project_extras from core.utils.util import get_labeled_data @@ -136,6 +136,30 @@ def download_model(request, project_pk, unverified): return response +@api_view(["GET"]) +@permission_classes((IsAdminOrCreator,)) +def download_irr_log(request, project_pk): + response = HttpResponse( + content_type="text/csv", + headers={ + "Content-Disposition": f'attachment; filename="irr_log_{project_pk}.csv"' + }, + ) + + writer = csv.writer(response) + writer.writerow(["text", "label", "username", "timestamp"]) + + logs = IRRLog.objects.filter(data__project_id=project_pk).select_related( + "data", "profile", "label" + ) + + for log in logs: + label_name = log.label.name if log.label else "" + writer.writerow([log.data.text, label_name, log.profile.user, log.timestamp]) + + return response + + @api_view(["POST"]) @permission_classes((IsAdminOrCreator,)) def import_database_table(request, project_pk): diff --git a/backend/django/core/views/api_admin.py b/backend/django/core/views/api_admin.py index 7863e986..ac4f65d2 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, @@ -287,6 +288,27 @@ def perc_agree_table(request, project_pk): return Response({"data": user_agree}) +@api_view(["GET"]) +@permission_classes((IsAdminOrCreator,)) +def irr_log(request, project_pk): + """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) + + admin_queue_only = request.query_params.get("admin", "false").lower() == "true" + + irr_log = IRRLog.objects.filter(data__project=project) + if admin_queue_only: + irr_log = irr_log.filter(data__queues__type="admin") + + irr_log_serialized = IRRLogModelSerializer(irr_log, many=True).data + + return Response({"irr_log": irr_log_serialized}) + + @api_view(["GET"]) @permission_classes((IsAdminOrCreator,)) def heat_map_data(request, project_pk): diff --git a/backend/django/core/views/api_annotate.py b/backend/django/core/views/api_annotate.py index af252025..2e3d885a 100644 --- a/backend/django/core/views/api_annotate.py +++ b/backend/django/core/views/api_annotate.py @@ -94,18 +94,15 @@ def get_labels(request, project_pk): labels: The project labels """ project = Project.objects.get(pk=project_pk) - labels = Label.objects.all().filter(project=project) + labels = Label.objects.filter(project=project) + total_labels = Label.objects.filter(project=project).count() # If the number of labels is > 100, just return the first 100 serialized_labels = LabelSerializer(labels, many=True).data if len(serialized_labels) > 100: serialized_labels = serialized_labels[:100] - return Response( - { - "labels": serialized_labels, - } - ) + return Response({"labels": serialized_labels, "total_labels": total_labels}) @api_view(["GET"]) @@ -999,37 +996,91 @@ def get_label_history(request, project_pk): project = Project.objects.get(pk=project_pk) labels = Label.objects.all().filter(project=project) - # irr data gets set to not IRR once it's finalized - finalized_irr_data = IRRLog.objects.filter(data__irr_ind=False).values_list( - "data__pk", flat=True + + permission_filter = Q() + if project_extras.proj_permission_level(project, profile) < 2: + permission_filter &= Q(profile=profile) + + admin_queue_data = DataQueue.objects.filter( + queue__project=project, queue__type="admin" + ).values_list("data__pk", flat=True) + + pending_irr_data = IRRLog.objects.filter( + permission_filter & Q(data__in=admin_queue_data) + ).values_list("data__pk", flat=True) + + incomplete_irr_data_labeled = DataLabel.objects.filter( + profile=profile, data__project=project_pk, data__irr_ind=True + ).values_list("data__pk", flat=True) + # incomplete irr data that was sent to adjudication is unlabeled - so in the IRRLog table, not DataLabel + incomplete_irr_data_adjudicated = ( + IRRLog.objects.filter( + profile=profile, data__project=project_pk, data__irr_ind=True + ) + .exclude(data__in=pending_irr_data) + .values_list("data__pk", flat=True) + ) + + incomplete_irr_data = list( + set(incomplete_irr_data_labeled) | set(incomplete_irr_data_adjudicated) ) + + # finalized and historic IRR data have the same data pk's - we distinguish them later in this function + finalized_irr_data = IRRLog.objects.filter( + permission_filter & Q(data__project=project_pk, data__irr_ind=False) + ).values_list("data__pk", flat=True) + + all_irr_data_pks = list( + set(incomplete_irr_data) | set(pending_irr_data) | set(finalized_irr_data) + ) + if ( project_extras.proj_permission_level(project, profile) >= 2 or project.allow_coders_view_labels ): labeled_data = DataLabel.objects.filter( - data__project=project_pk, label__in=labels - ).exclude(data__in=finalized_irr_data) + data__project=project_pk, label__in=labels, data__irr_ind=False + ).exclude(data__in=all_irr_data_pks) else: labeled_data = DataLabel.objects.filter( - profile=profile, data__project=project_pk, label__in=labels - ).exclude(data__in=finalized_irr_data) + profile=profile, + data__project=project_pk, + label__in=labels, + data__irr_ind=False, + ).exclude(data__in=all_irr_data_pks) labeled_data_list = list(labeled_data.values_list("data__pk", flat=True)) - # even for an admin, we only get personal IRR data - personal_irr_data = IRRLog.objects.filter( - profile=profile, data__project=project_pk, label__isnull=False - ).exclude(data__pk__in=labeled_data_list) - irr_data_list = list(personal_irr_data.values_list("data__pk", flat=True)) + # Incomplete and Finalized IRR are in Label table, not IRR Log table + all_relevant_irr_datalabels = DataLabel.objects.filter( + data__pk__in=all_irr_data_pks + ) # add the unlabeled data if selected - total_data_list = labeled_data_list + irr_data_list + unlabeled_data_list = [] if request.GET.get("unlabeled") == "true": - unlabeled_data = list( + unlabeled_data_list = list( get_unlabeled_data(project_pk).values_list("pk", flat=True) ) - total_data_list += unlabeled_data + total_data_list = list( + set(labeled_data_list + unlabeled_data_list + all_irr_data_pks) + ) + + data_types_dict = {} + for pk in total_data_list: + data_types_dict[pk] = "Labeled" + for pk in unlabeled_data_list: + data_types_dict[pk] = "Unlabeled" + for pk in incomplete_irr_data: + data_types_dict[pk] = "IRR Incomplete" + for pk in pending_irr_data: + data_types_dict[pk] = "IRR Pending" + for ( + pk + ) in ( + finalized_irr_data + ): # some "IRR Finalized" will be changed to "IRR Historic" later in this function + data_types_dict[pk] = "IRR Finalized" # return the page indicated in the query, get total pages current_page = request.GET.get("page") @@ -1040,21 +1091,21 @@ def get_label_history(request, project_pk): sort_by = request.GET.get("sort-by") reverse = request.GET.get("reverse", "false").lower() == "true" 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' + "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') - + + 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) + order_field = "-" + order_field + + all_data = Data.objects.filter(pk__in=data_types_dict.keys()).order_by(order_field) # filter the results by the search terms text_filter = request.GET.get("Text") @@ -1072,8 +1123,8 @@ def get_label_history(request, project_pk): 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_types = [{"pk": d.pk, "type": data_types_dict[d.pk]} for d in page_data] page_data_metadata_ids = [ d["metadata"] for d in DataMetadataIDSerializer(page_data, many=True).data @@ -1087,9 +1138,7 @@ def get_label_history(request, project_pk): ] 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 + data_types_df = pd.DataFrame(page_data_types).rename(columns={"pk": "id"}) if len(data_df) == 0: return Response( @@ -1100,6 +1149,11 @@ def get_label_history(request, project_pk): } ) + data_df = pd.merge(data_df, data_types_df, on="id") + data_df["metadataIDs"] = page_data_metadata_ids + data_df["metadata"] = page_metadata + data_df["formattedMetadata"] = page_metadata_formatted + # get the labeled data into the correct format for returning label_dict = {label.pk: label.name for label in labels} labeled_data_df = pd.DataFrame( @@ -1108,12 +1162,48 @@ def get_label_history(request, project_pk): ).data ).rename(columns={"data": "id", "label": "labelID", "verified": "verified_by"}) + all_relevant_irr_logs = IRRLog.objects.filter(data__pk__in=all_irr_data_pks) + + # contains all irr data that can be found in the IRR Log table ie pending and historic irr irr_data_df = pd.DataFrame( IRRLogModelSerializer( - personal_irr_data.filter(data__pk__in=data_df["id"].tolist()), many=True + all_relevant_irr_logs.filter( + permission_filter & Q(data__pk__in=data_df["id"].tolist()) + ), + many=True, + ).data + ).rename(columns={"data": "id", "label": "labelID"}) + + if not irr_data_df.empty: + # finalized_irr_data contains both finalized and historic irr data. It's historic if in this df. + irr_data_df["is_historic"] = irr_data_df["id"].apply( + lambda x: x in finalized_irr_data + ) + else: + irr_data_df["is_historic"] = pd.Series(dtype=bool) + + irr_incomplete_df = pd.DataFrame( + DataLabelModelSerializer( + all_relevant_irr_datalabels.filter( + profile=profile, data__pk__in=list(set(incomplete_irr_data)) + ), + many=True, + ).data + ).rename(columns={"data": "id", "label": "labelID"}) + + irr_finalized_df = pd.DataFrame( + DataLabelModelSerializer( + all_relevant_irr_datalabels.filter( + permission_filter & Q(data__pk__in=list(set(finalized_irr_data))) + ), + many=True, ).data ).rename(columns={"data": "id", "label": "labelID"}) + irr_data_df = pd.concat( + [irr_data_df, irr_incomplete_df, irr_finalized_df], axis=0 + ).reset_index(drop=True) + if len(labeled_data_df) > 0: labeled_data_df["verified"] = labeled_data_df["verified_by"].apply( lambda x: "No" if x is None else "Yes" @@ -1130,9 +1220,11 @@ def get_label_history(request, project_pk): if len(irr_data_df) > 0: irr_data_df["edit"] = "No" - irr_data_df["label"] = irr_data_df["labelID"].apply(lambda x: label_dict[x]) + irr_data_df["label"] = irr_data_df["labelID"].apply( + lambda x: label_dict[x] if x in label_dict else "" + ) irr_data_df["verified"] = ( - "N/A (IRR)" # Technically resolved IRR is verified but perhaps not this user's specific label so just NA + "N/A (IRR)" # Technically finalized IRR is verified but perhaps not this user's specific label so just NA ) irr_data_df["verified_by"] = None irr_data_df["pre_loaded"] = "No" # IRR only looks at unlabeled data @@ -1144,7 +1236,17 @@ def get_label_history(request, project_pk): # merge the data info with the label info if len(all_labeled_stuff) > 0: data_df = data_df.merge(all_labeled_stuff, on=["id"], how="left") + data_df.loc[data_df["is_historic"] == True, "type"] = "IRR Historic" + data_df.drop(columns=["is_historic"], inplace=True) data_df["edit"] = data_df["edit"].fillna("Yes") + data_df.loc[(data_df["type"] == "IRR Incomplete"), "edit"] = "Yes" + data_df.loc[ + (data_df["type"] == "IRR Incomplete") & (data_df["label"] == ""), "edit" + ] = "No" + data_df.loc[ + (data_df["type"] == "IRR Pending") & (data_df["profile"].isna()), "edit" + ] = "No" + data_df.loc[(data_df["type"] == "IRR Finalized"), "edit"] = "Yes" else: data_df["edit"] = "Yes" data_df["label"] = "" diff --git a/backend/django/core/views/frontend.py b/backend/django/core/views/frontend.py index 2bc756d9..0256bc89 100644 --- a/backend/django/core/views/frontend.py +++ b/backend/django/core/views/frontend.py @@ -10,6 +10,7 @@ from django.views.generic import DetailView, ListView, TemplateView, View from django.views.generic.edit import DeleteView, UpdateView from formtools.wizard.views import SessionWizardView +from smart.settings import PROJECT_SUGGESTION_MAX from core.forms import ( AdvancedWizardForm, @@ -81,6 +82,8 @@ def get_context_data(self, **kwargs): else: ctx["project_uses_irr"] = "false" + ctx["project_suggestion_max"] = PROJECT_SUGGESTION_MAX + return ctx diff --git a/backend/django/smart/settings.py b/backend/django/smart/settings.py index cdc262c1..fcfde60e 100644 --- a/backend/django/smart/settings.py +++ b/backend/django/smart/settings.py @@ -227,6 +227,7 @@ class Dev(Configuration): DATA_UPLOAD_MAX_MEMORY_SIZE = None ADMIN_TIMEOUT_MINUTES = 15 + PROJECT_SUGGESTION_MAX = os.environ.get("PROJECT_SUGGESTION_MAX", 10000) class Prod(Dev): diff --git a/frontend/src/actions/adminTables.js b/frontend/src/actions/adminTables.js index b93a0f43..f5ec30c9 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,32 @@ export const getAdmin = (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 => { + 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.irr_log)); + } + }); + }; +}; + 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..b37f79a4 --- /dev/null +++ b/frontend/src/components/AdminTable/IRRtable.jsx @@ -0,0 +1,32 @@ +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; + } + return ( + + + + + + + + + + + {Object.entries(irrEntry).map(([user, label], index) => ( + + + + + + ))} + +
UserLabelDescription
{user}{label.name}{label.description}
+
+ ); +}; + +export default IRRtable; diff --git a/frontend/src/components/AdminTable/index.jsx b/frontend/src/components/AdminTable/index.jsx index c3e523be..d5740b9f 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,22 @@ class AdminTable extends React.Component { } render() { - const { admin_data, labels, message, adminLabel, discardData } = this.props; + const { admin_data, irr_log, message, adminLabel, discardData } = this.props; + + const getIrrEntry = data_id => { + const relevant_irr_entries = irr_log.filter(entry => entry.data === data_id); + const irr_entry_formatted = {}; + for (let entry of relevant_irr_entries) { + const username = entry.profile; + if (entry.label === null) { + // situation where the irr data was adjudicated instead of labeled + irr_entry_formatted[username] = { name: "", description: "" }; + } else { + irr_entry_formatted[username] = { name: entry.label_name, description: entry.label_description }; + } + } + return irr_entry_formatted; + }; const columns = [ { @@ -57,15 +74,22 @@ class AdminTable extends React.Component {

{row.original.message}

)} - +
+ + { row.original.reason === "IRR" && irr_log.length && + + } +
); } - } + }, + // column for coder, label table + ]; let page_sizes = [1]; @@ -118,7 +142,6 @@ class AdminTable extends React.Component { AdminTable.propTypes = { getAdmin: PropTypes.func.isRequired, admin_data: PropTypes.arrayOf(PropTypes.object), - labels: PropTypes.arrayOf(PropTypes.object), message: PropTypes.string, adminLabel: PropTypes.func.isRequired, discardData: PropTypes.func.isRequired diff --git a/frontend/src/components/DataCard/DataCard.jsx b/frontend/src/components/DataCard/DataCard.jsx index efdf36d3..0b8752a8 100644 --- a/frontend/src/components/DataCard/DataCard.jsx +++ b/frontend/src/components/DataCard/DataCard.jsx @@ -10,6 +10,7 @@ import DataCardText from "./DataCardText"; import { useModifyLabel, useChangeToSkip, useLabels } from "../../hooks"; import DataCardLabelButtons from "./DataCardLabelButtons"; import DataCardDiscardButton from "./DataCardDiscardButton"; +import { PROJECT_SUGGESTION_MAX } from "../../store"; const DataCard = ({ data, page, actions }) => { const { data: labels } = useLabels(); @@ -24,6 +25,7 @@ const DataCard = ({ data, page, actions }) => { const handlers = getHandlers(allHandlers, page); const labelCountLow = (labels) => labels.labels.length <= 5; + const labelCountHigh = (labels) => labels.total_labels >= PROJECT_SUGGESTION_MAX; const show = { skipButton: handlers.handleSkip != null, @@ -31,9 +33,9 @@ const DataCard = ({ data, page, actions }) => { text: true, metadata: true, metadataEdit: page !== PAGES.RECYCLE, - labelButtons: labels && labelCountLow(labels) && handlers.handleSelectLabel != null, - labelSuggestions: labels && !labelCountLow(labels) && handlers.handleSelectLabel != null, - labelSelect: labels && !labelCountLow(labels) && handlers.handleSelectLabel != null, + labelButtons: labels && labelCountLow(labels) && (handlers.handleSelectLabel != null), + labelSuggestions: labels && (!labelCountLow(labels)) && (!labelCountHigh(labels)) && (handlers.handleSelectLabel != null), + labelSelect: labels && (!labelCountLow(labels)) && (handlers.handleSelectLabel != null), discardButton: handlers.handleDiscard != null, confirmationModal: page == PAGES.HISTORY && cardData.labelID // excludes unlabeled data }; @@ -78,6 +80,10 @@ const DataCard = ({ data, page, actions }) => { fn= { handlers.handleSelectLabel } includeModal={show.confirmationModal} /> + + )} + {show.labelSelect && ( +
null, id: "Expander" }, + { + accessorKey: "type", + filterFn: "includesString", + header: "Type", + id: "Type", + sortingFn: "alphanumeric" + }, { accessorKey: "data", filterFn: "includesString", diff --git a/frontend/src/containers/adminTable_container.jsx b/frontend/src/containers/adminTable_container.jsx index 5950c237..d886bc6c 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,7 +11,7 @@ const AdminTableContainer = (props) => ; const mapStateToProps = (state) => { return { admin_data: state.adminTables.admin_data, - labels: state.smart.labels, + irr_log: state.adminTables.irr_log, message: state.card.message, admin_counts: state.adminTables.admin_counts }; @@ -25,6 +25,9 @@ const mapDispatchToProps = (dispatch) => { getAdmin: () => { dispatch(getAdmin(PROJECT_ID)); }, + getIrrLog: () => { + dispatch(getIrrLog(PROJECT_ID, true)); + }, 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/store.js b/frontend/src/store.js index 1083b2d9..3cd146c3 100644 --- a/frontend/src/store.js +++ b/frontend/src/store.js @@ -4,4 +4,6 @@ export const PROJECT_ID = window.PROJECT_ID; export const PROJECT_USES_IRR = window.PROJECT_USES_IRR; +export const PROJECT_SUGGESTION_MAX = window.PROJECT_SUGGESTION_MAX; + export const queryClient = new QueryClient(); diff --git a/frontend/src/styles/smart.scss b/frontend/src/styles/smart.scss index 94d84381..f86b87ed 100644 --- a/frontend/src/styles/smart.scss +++ b/frontend/src/styles/smart.scss @@ -751,3 +751,25 @@ li.disabled { margin-right: 5px; } } + +.admin-data-card-wrapper { + display: flex; + + > :first-child { + flex-grow: 1; + display: block !important; + + p { + max-width: 500px; + } + } + + .irr-card { + width: 450px; + overflow-x: scroll + } + + .btn-toolbar { + display: block; + } +} \ No newline at end of file