From 4806d06ef0fdeb1146398272089d14aa463d6ba5 Mon Sep 17 00:00:00 2001 From: Zakki Date: Wed, 2 Aug 2023 10:32:35 +0700 Subject: [PATCH 1/7] Update Review-List endpoint, Review filter value, add store --- django_project/dashboard/api_views/reviews.py | 197 ++++++++++-- .../dashboard/models/entity_upload.py | 19 ++ django_project/dashboard/src/app/store.ts | 4 +- .../dashboard/src/reducers/reviewTable.ts | 52 ++++ .../dashboard/src/views/Review/Filter.ts | 19 ++ .../dashboard/src/views/Review/List.tsx | 283 +++++++++++++++++- django_project/dashboard/urls.py | 7 + 7 files changed, 546 insertions(+), 35 deletions(-) create mode 100644 django_project/dashboard/src/reducers/reviewTable.ts create mode 100644 django_project/dashboard/src/views/Review/Filter.ts diff --git a/django_project/dashboard/api_views/reviews.py b/django_project/dashboard/api_views/reviews.py index 9581e27e..65fd46f9 100644 --- a/django_project/dashboard/api_views/reviews.py +++ b/django_project/dashboard/api_views/reviews.py @@ -1,12 +1,17 @@ +import math -from django.shortcuts import get_object_or_404 from django.core.exceptions import PermissionDenied +from django.core.paginator import Paginator +from django.db.models import Q +from django.shortcuts import get_object_or_404 from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView + from azure_auth.backends import AzureAuthRequiredMixin -from georepo.models.dataset import Dataset -from georepo.utils.permission import get_dataset_to_review +from dashboard.api_views.common import ( + DatasetWritePermission +) from dashboard.models import ( EntityUploadStatus, LayerUploadSession, REVIEWING @@ -17,18 +22,16 @@ from dashboard.models.entity_upload import ( REVIEWING as UPLOAD_REVIEWING, PROCESSING_APPROVAL, - APPROVED + APPROVED, + REJECTED ) from dashboard.serializers.entity_upload import EntityUploadSerializer from dashboard.tasks import run_comparison_boundary -from georepo.utils.module_import import module_function -from dashboard.api_views.common import ( - DatasetWritePermission -) from dashboard.tasks.review import ( review_approval, process_batch_review ) +from georepo.utils.module_import import module_function class ReadyToReview(AzureAuthRequiredMixin, APIView): @@ -137,23 +140,169 @@ class ReviewList(AzureAuthRequiredMixin, APIView): """Api to list all ready to review uploads""" permission_classes = [IsAuthenticated] + def _filter_queryset(self, request, queryset): + criteria_field_mapping = { + 'level_0_entity': 'revised_geographical_entity__label', + 'upload': 'upload_session__source', + 'revision': 'revised_geographical_entity__revision_number', + 'dataset': 'upload_session__dataset__label' + } + + filter_kwargs = {} + for filter_field, model_field in criteria_field_mapping.items(): + filter_values = dict(request.data).get(filter_field, []) + if not filter_values: + continue + filter_kwargs.update({f'{model_field}__in': filter_values}) + + if 'status' in dict(request.data): + filter_values = sorted(dict(request.data).get('status', [])) + if not filter_values or filter_values == [APPROVED, 'Pending', REJECTED]: + return {} + + non_pending_filter_combinations = [ + [APPROVED], + [REJECTED], + [APPROVED, REJECTED] + ] + pending_status = [ + choice[0] for choice in EntityUploadStatus.STATUS_CHOICES if + choice[0] not in [APPROVED, REJECTED] + ] + if filter_values in non_pending_filter_combinations: + filter_kwargs.update({'status__in': filter_values}) + elif 'Pending' in filter_values: + if APPROVED in filter_values: + filter_kwargs.update({'status__in': [*pending_status, APPROVED]}) + elif REJECTED in filter_values: + filter_kwargs.update({'status__in': [*pending_status, REJECTED]}) + else: + filter_kwargs.update({'status__in': [pending_status]}) + + return queryset.filter(**filter_kwargs) + + def _search_queryset(self, queryset, request): + search_text = request.data.get('search_text', '') + if not search_text: + return queryset + char_fields = [ + field.name for field in EntityUploadStatus.get_fields() if + field.get_internal_type() in + ['UUIDField', 'CharField', 'TextField'] + ] + q_args = [ + Q(**{f"{field}__icontains": search_text}) for field in char_fields + ] + args = Q() + for arg in q_args: + args |= arg + queryset = queryset.filter(*(args,)) + return queryset + + def _sort_queryset(self, queryset, request): + sort_by = request.query_params.get('sort_by', 'name') + sort_direction = request.query_params.get('sort_direction', 'asc') + if not sort_by: + sort_by = 'name' + if not sort_direction: + sort_direction = 'asc' + ordering = sort_by if sort_direction == 'asc' else f"-{sort_by}" + queryset = queryset.order_by(ordering) + return queryset + def get(self, request, *args, **kwargs): - datasets = Dataset.objects.all().order_by('created_at') - datasets = get_dataset_to_review( - self.request.user, - datasets - ) - entity_uploads = EntityUploadStatus.objects.filter( - status__in=[REVIEWING, APPROVED], - upload_session__dataset__in=datasets - ).order_by('-started_at') - if not self.request.user.is_superuser: - entity_uploads = entity_uploads.exclude( - upload_session__uploader=self.request.user - ) - return Response( - EntityUploadSerializer(entity_uploads, many=True).data - ) + review_querysets = EntityUploadStatus.get_user_entity_upload_status(request.user) + review_querysets = self._search_queryset(review_querysets, self.request) + review_querysets = self._filter_queryset(review_querysets, self.request) + page = int(self.request.GET.get('page', '1')) + page_size = int(self.request.query_params.get('page_size', '10')) + review_querysets = self._sort_queryset(review_querysets, self.request) + paginator = Paginator(review_querysets, page_size) + total_page = math.ceil(paginator.count / page_size) + if page > total_page: + output = [] + else: + paginated_entities = paginator.get_page(page) + output = EntityUploadSerializer( + paginated_entities, + many=True + ).data + return Response({ + 'count': paginator.count, + 'page': page, + 'total_page': total_page, + 'page_size': page_size, + 'results': output, + }) + + +class ReviewFilterValue( + AzureAuthRequiredMixin, + APIView +): + """ + Get filter value for given Review and criteria + """ + permission_classes = [IsAuthenticated] + review_querysets = EntityUploadStatus.objects.none() + + def fetch_criteria_values(self, criteria): + criteria_field_mapping = { + 'level_0_entity': 'revised_geographical_entity__label', + 'upload': 'upload_session__source', + 'revision': 'revised_geographical_entity__revision_number', + } + field = criteria_field_mapping.get(criteria, None) + + if not field: + if criteria == 'dataset': + return self.fetch_dataset() + return self.fetch_status() + + filter_values = self.reviews_querysets.\ + filter(**{f"{field}__isnull": False}).order_by().\ + values_list(field, flat=True).distinct() + return [val for val in filter_values] + + def fetch_level_0_entity(self): + filter_values = self.reviews_querysets.\ + filter(revised_geographical_entity__label__isnull=False).order_by().\ + values_list('revised_geographical_entity__label', flat=True).distinct() + return [val for val in filter_values] + + def fetch_upload(self): + filter_values = self.reviews_querysets. \ + filter(upload_session__source__isnull=False).order_by(). \ + values_list('upload_session__source', flat=True).distinct() + return [val for val in filter_values] + + def fetch_dataset(self): + return self.reviews_querysets.filter( + upload_session__dataset__label__isnull=False + ).exclude( + upload_session__dataset__label__exact='' + ).order_by().values_list('upload_session__dataset__label', flat=True).distinct() + + def fetch_revision(self): + filter_values = self.reviews_querysets. \ + filter(revised_geographical_entity__revision_number__isnull=False).order_by(). \ + values_list('revised_geographical_entity__revision_number', flat=True).distinct() + return [val for val in filter_values] + + def fetch_status(self): + return [ + APPROVED, + REJECTED, + 'Pending' + ] + + def get(self, request, criteria, *args, **kwargs): + self.reviews_querysets = EntityUploadStatus.get_user_entity_upload_status(request.user) + try: + data = self.fetch_criteria_values(criteria) + except AttributeError: + data = [] + return Response(status=200, data=data) class ApproveRevision(AzureAuthRequiredMixin, diff --git a/django_project/dashboard/models/entity_upload.py b/django_project/dashboard/models/entity_upload.py index 750cb19d..b1607cdb 100644 --- a/django_project/dashboard/models/entity_upload.py +++ b/django_project/dashboard/models/entity_upload.py @@ -1,6 +1,8 @@ from django.db import models from django.db.models.signals import pre_delete from django.dispatch import receiver +from georepo.models.dataset import Dataset +from georepo.utils.permission import get_dataset_to_review PROCESSING = 'Processing' VALID = 'Valid' @@ -163,6 +165,23 @@ def get_entity_admin_level_name(self, level: int) -> str | None: adm_level_name = self.admin_level_names[level_str] return adm_level_name + @classmethod + def get_user_entity_upload_status(cls, user): + datasets = Dataset.objects.all().order_by('created_at') + datasets = get_dataset_to_review( + user, + datasets + ) + entity_uploads = cls.objects.filter( + status__in=[REVIEWING, APPROVED], + upload_session__dataset__in=datasets + ).order_by('-started_at') + if not user.is_superuser: + entity_uploads = entity_uploads.exclude( + upload_session__uploader=user + ) + return entity_uploads + @receiver(pre_delete, sender=EntityUploadStatus) def delete_entity_upload(sender, instance, **kwargs): diff --git a/django_project/dashboard/src/app/store.ts b/django_project/dashboard/src/app/store.ts index 8813c0b1..c596a330 100644 --- a/django_project/dashboard/src/app/store.ts +++ b/django_project/dashboard/src/app/store.ts @@ -5,6 +5,7 @@ import moduleReducer from "../reducers/module" import pollIntervalReducer from "../reducers/notificationPoll" import maintenanceReducer from "../reducers/maintenanceItem" import reviewActionReducer from "../reducers/reviewAction" +import reviewTableReducer from "../reducers/reviewTable" export const store = configureStore({ reducer: { @@ -13,7 +14,8 @@ export const store = configureStore({ module: moduleReducer, pollInterval: pollIntervalReducer, maintenanceItem: maintenanceReducer, - reviewAction: reviewActionReducer + reviewAction: reviewActionReducer, + reviewTable: reviewTableReducer }, }); diff --git a/django_project/dashboard/src/reducers/reviewTable.ts b/django_project/dashboard/src/reducers/reviewTable.ts new file mode 100644 index 00000000..20ff7970 --- /dev/null +++ b/django_project/dashboard/src/reducers/reviewTable.ts @@ -0,0 +1,52 @@ +import {createSlice, PayloadAction} from "@reduxjs/toolkit"; +import {getDefaultFilter, ReviewFilterInterface} from "../views/Review/Filter" +import {RootState} from "../app/store"; + + +export interface TableState { + currentColumns: string[]; + currentFilters: ReviewFilterInterface; + availableFilters: ReviewFilterInterface; +} + +const initialState: TableState = { + currentColumns: [ + 'level_0_entity', + 'upload', + 'dataset', + 'start_date', + 'revision', + 'status', + 'submitted_by' + ], + currentFilters: getDefaultFilter(), + availableFilters: getDefaultFilter() +}; + +export const reviewTableSlice = createSlice({ + name: 'reviewTable', + initialState, + reducers: { + setCurrentColumns: (state, action: PayloadAction) => { + state.currentColumns = JSON.parse(action.payload) + }, + setCurrentFilters: (state, action: PayloadAction) => { + state.currentFilters = JSON.parse(action.payload) + }, + setAvailableFilters: (state, action: PayloadAction) => { + state.availableFilters = JSON.parse(action.payload) + } + } +}) + +export const { + setCurrentColumns, + setCurrentFilters, + setAvailableFilters +} = reviewTableSlice.actions + +export default reviewTableSlice.reducer; + +export const currentColumns = (state: RootState) => state.reviewTable.currentColumns +export const currentFilters = (state: RootState) => state.reviewTable.currentFilters +export const availableFilters = (state: RootState) => state.reviewTable.availableFilters \ No newline at end of file diff --git a/django_project/dashboard/src/views/Review/Filter.ts b/django_project/dashboard/src/views/Review/Filter.ts new file mode 100644 index 00000000..a2b64a11 --- /dev/null +++ b/django_project/dashboard/src/views/Review/Filter.ts @@ -0,0 +1,19 @@ +export interface ReviewFilterInterface { + level_0_entity: string[], + upload: string[], + revision: string[], + dataset: string[], + status: string[], + search_text: string +} + +export function getDefaultFilter():ReviewFilterInterface { + return { + level_0_entity: [], + upload: [], + revision: [], + dataset: [], + status: [], + search_text: '' + } +} \ No newline at end of file diff --git a/django_project/dashboard/src/views/Review/List.tsx b/django_project/dashboard/src/views/Review/List.tsx index ebbfe27f..1f6d8966 100644 --- a/django_project/dashboard/src/views/Review/List.tsx +++ b/django_project/dashboard/src/views/Review/List.tsx @@ -9,6 +9,42 @@ import {modules} from "../../modules"; import List from "../../components/List"; import Loading from "../../components/Loading"; import {setSelectedReviews} from "../../reducers/reviewAction"; +import MUIDataTable, {debounceSearchRender, MUISortOptions} from "mui-datatables"; +import PaginationInterface, {getDefaultPagination, rowsPerPageOptions} from "../../models/pagination"; +import {getDefaultFilter, ReviewFilterInterface} from "./Filter" +import { + setAvailableFilters, + setCurrentColumns as setInitialColumns, + setCurrentFilters as setInitialFilters +} from "../../reducers/reviewTable"; +import {RootState} from "../../app/store"; + + +const USER_COLUMNS = [ + 'id', + 'level_0_entity', + 'upload', + 'dataset', + 'start_date', + 'revision', + 'status', + 'submitted_by', + 'module', + 'is_comparison_ready' +] + +interface reviewTableRowInterface { + id: number, + level_0_entity: string, + upload: string, + dataset: string, + start_date: string, + revision: number, + status: string, + submitted_by: string, + module: string, + is_comparison_ready: string +} export interface ReviewData { @@ -17,8 +53,13 @@ export interface ReviewData { module: string } +const FILTER_VALUES_API_URL = '/api/review-filter/values/' + export default function ReviewList () { + const initialColumns = useAppSelector((state: RootState) => state.reviewTable.currentColumns) + const initialFilters = useAppSelector((state: RootState) => state.reviewTable.currentFilters) + const availableFilters = useAppSelector((state: RootState) => state.reviewTable.availableFilters) const [loading, setLoading] = useState(true) const [searchParams, setSearchParams] = useSearchParams() const [data, setData] = useState([]) @@ -30,22 +71,244 @@ export default function ReviewList () { const pendingReviews = useAppSelector((state: RootState) => state.reviewAction.pendingReviews) const reviewUpdatedAt = useAppSelector((state: RootState) => state.reviewAction.updatedAt) + const [columns, setColumns] = useState([]) + const [totalCount, setTotalCount] = useState(0) + const [pagination, setPagination] = useState(getDefaultPagination()) + const [filterValues, setFilterValues] = useState(availableFilters) + const [currentFilters, setCurrentFilters] = useState(initialFilters) + + const fetchFilterValues = async () => { + let filters = [] + filters.push(axios.get(`${FILTER_VALUES_API_URL}level_0_entity/`)) + filters.push(axios.get(`${FILTER_VALUES_API_URL}upload/`)) + filters.push(axios.get(`${FILTER_VALUES_API_URL}dataset/`)) + filters.push(axios.get(`${FILTER_VALUES_API_URL}revision/`)) + filters.push(axios.get(`${FILTER_VALUES_API_URL}status/`)) + let resultData = await Promise.all(filters) + let filterVals = { + 'level_0_entity': resultData[0].data, + 'upload': resultData[1].data, + 'dataset': resultData[2].data, + 'revision': resultData[3].data, + 'status': resultData[4].data, + 'search_text': '' + } + setFilterValues(filterVals) + dispatch(setAvailableFilters(JSON.stringify(filterVals))) + return filterVals + } + const fetchReviewList = () => { - axios.get('/api/review-list').then((response) => { + if (axiosSource.current) axiosSource.current.cancel() + let cancelFetchToken = newCancelToken() + setLoading(true) + let sortBy = pagination.sortOrder.name ? pagination.sortOrder.name : '' + let sortDirection = pagination.sortOrder.direction ? pagination.sortOrder.direction : '' + const url = `${VIEW_LIST_URL}?` + `page=${pagination.page + 1}&page_size=${pagination.rowsPerPage}` + + `&sort_by=${sortBy}&sort_direction=${sortDirection}` + axios.post( + url, + currentFilters, + { + cancelToken: cancelFetchToken + } + ).then((response) => { setLoading(false) - if (response.data) { // update filter values if searchParams has upload filter - let _upload = searchParams.get('upload') - if (_upload) { - setCustomOptions({ - 'upload': { - 'filterList': [_upload] - } - }) + // let _upload = searchParams.get('upload') + // if (_upload) { + // setCustomOptions({ + // 'upload': { + // 'filterList': [_upload] + // } + // }) + setLoading(false) + setData(response.data.results as ReviewFilterInterface[]) + setTotalCount(response.data.count) + }) + } + + const getExistingFilterValue = (colName: string): string[] => { + let values: string[] = [] + switch (colName) { + case 'level_0_entity': + values = currentFilters.level_0_entity + break; + case 'upload': + values = currentFilters.upload + break; + case 'dataset': + values = currentFilters.dataset + break; + case 'revision': + values = currentFilters.revision + break; + case 'status': + values = currentFilters.status + break; + default: + break; + } + return values + } + + useEffect(() => { + const fetchFilterValuesData = async () => { + + let filterVals: any = {} + if (filterValues.mode.length > 0 ) { + filterVals = filterValues + } else { + filterVals = await fetchFilterValues() + } + let _init_columns = USER_COLUMNS + let _columns = _init_columns.map((columnName) => { + let _options: any = { + name: columnName, + label: columnName.charAt(0).toUpperCase() + columnName.slice(1).replaceAll('_', ' '), + options: { + display: initialColumns.includes(columnName), + sort: !['tags', 'permissions', 'status', 'mode'].includes(columnName) + } + } + if (columnName == 'tags') { + _options['options']['filterType'] = 'multiselect' + _options['options']['filter'] = true + _options['options']['customBodyRender'] = (value: any, tableMeta: any) => { + return
+ {value.map((tag: any, index: number) => )} +
+ } + } + if (['tags', 'mode', 'dataset', 'is_default', 'min_privacy', 'max_privacy'].includes(columnName)) { + // set filter values in dropdown + _options.options.filterOptions = { + names: filterVals[columnName] + } + _options.options.filter = true + } else { + _options.options.filter = false + } + if (['tags', 'mode', 'dataset', 'is_default', 'min_privacy', 'max_privacy'].includes(columnName)) { + // set existing filter values + _options.options.filterList = getExistingFilterValue(columnName) } - setData(response.data) + return _options + }) + _columns.push({ + name: '', + options: { + customBodyRender: (value: any, tableMeta: any, updateValue: any) => { + let rowData = tableMeta.rowData + return ( +
+ ) => { + event.stopPropagation(); + let obj: any = {} + USER_COLUMNS.forEach((element, index) => { + obj[element] = rowData[index]; + }); + setSelectedView(obj) + setAnchorEl(event.currentTarget); + }} + className='' + > + + + + { + setSelectedView(rowData) + setConfirmationText( + `Are you sure you want to delete ${rowData[1]}?`) + setConfirmationOpen(true) + }} + className='' + > + + +
+ ) + }, + filter: false + } + }) + setColumns(_columns) + dispatch(setInitialColumns(JSON.stringify(_columns.map))) + } + fetchFilterValuesData() + }, [pagination, currentFilters]) + + useEffect(() => { + fetchReviewList() + }, [pagination, filterValues, currentFilters]) + + const onTableChangeState = (action: string, tableState: any) => { + switch (action) { + case 'changePage': + setPagination({ + ...pagination, + page: tableState.page + }) + break; + case 'sort': + setPagination({ + ...pagination, + page: 0, + sortOrder: tableState.sortOrder + }) + break; + case 'changeRowsPerPage': + setPagination({ + ...pagination, + page: 0, + rowsPerPage: tableState.rowsPerPage + }) + break; + default: + } + } + + const handleFilterSubmit = (applyFilters: any) => { + let filterList = applyFilters() + let filter = getDefaultFilter() + type Column = { + name: string, + label: string, + options: any + } + for (let idx in filterList) { + let col: Column = columns[idx] + if (!col.options.filter) + continue + if (filterList[idx] && filterList[idx].length) { + const key = col.name as string + filter[key as keyof ReviewFilterInterface] = filterList[idx] } + } + setCurrentFilters({...filter, 'search_text': currentFilters['search_text']}) + dispatch(setInitialFilters(JSON.stringify({...filter, 'search_text': currentFilters['search_text']}))) + } + + const handleSearchOnChange = (search_text: string) => { + setPagination({ + ...pagination, + page: 0, + sortOrder: {} }) + setCurrentFilters({...currentFilters, 'search_text': search_text}) + dispatch(setInitialFilters(JSON.stringify({...currentFilters, 'search_text': search_text}))) } useEffect(() => { diff --git a/django_project/dashboard/urls.py b/django_project/dashboard/urls.py index 9caee397..950ef9e1 100644 --- a/django_project/dashboard/urls.py +++ b/django_project/dashboard/urls.py @@ -3,6 +3,7 @@ from dashboard.api_views.reviews import ( ReadyToReview, ReviewList, + ReviewFilterValue, ApproveRevision, RejectRevision, BatchReviewAPI, @@ -342,6 +343,12 @@ ReviewList.as_view(), name='review-list' ), + re_path( + r'^api/review-filter/values/' + r'(?P\w+)/?$', + ReviewFilterValue.as_view(), + name='review-filter-value' + ), re_path( r'api/boundary-comparison-summary/(?P\d+)/?$', BoundaryComparisonSummary.as_view(), From ad479aa41da34da0324acb29507f18754dfa45d8 Mon Sep 17 00:00:00 2001 From: Zakki Date: Wed, 2 Aug 2023 13:49:59 +0700 Subject: [PATCH 2/7] Add server side Review Listing page --- django_project/dashboard/api_views/reviews.py | 14 +- .../dashboard/src/views/Review/List.tsx | 218 ++++++++++-------- 2 files changed, 127 insertions(+), 105 deletions(-) diff --git a/django_project/dashboard/api_views/reviews.py b/django_project/dashboard/api_views/reviews.py index 65fd46f9..222690f2 100644 --- a/django_project/dashboard/api_views/reviews.py +++ b/django_project/dashboard/api_views/reviews.py @@ -140,7 +140,7 @@ class ReviewList(AzureAuthRequiredMixin, APIView): """Api to list all ready to review uploads""" permission_classes = [IsAuthenticated] - def _filter_queryset(self, request, queryset): + def _filter_queryset(self, queryset, request): criteria_field_mapping = { 'level_0_entity': 'revised_geographical_entity__label', 'upload': 'upload_session__source', @@ -158,7 +158,7 @@ def _filter_queryset(self, request, queryset): if 'status' in dict(request.data): filter_values = sorted(dict(request.data).get('status', [])) if not filter_values or filter_values == [APPROVED, 'Pending', REJECTED]: - return {} + return queryset.filter(**filter_kwargs) non_pending_filter_combinations = [ [APPROVED], @@ -186,7 +186,7 @@ def _search_queryset(self, queryset, request): if not search_text: return queryset char_fields = [ - field.name for field in EntityUploadStatus.get_fields() if + field.name for field in EntityUploadStatus._meta.get_fields() if field.get_internal_type() in ['UUIDField', 'CharField', 'TextField'] ] @@ -200,17 +200,17 @@ def _search_queryset(self, queryset, request): return queryset def _sort_queryset(self, queryset, request): - sort_by = request.query_params.get('sort_by', 'name') + sort_by = request.query_params.get('sort_by', 'id') sort_direction = request.query_params.get('sort_direction', 'asc') if not sort_by: - sort_by = 'name' + sort_by = 'id' if not sort_direction: sort_direction = 'asc' ordering = sort_by if sort_direction == 'asc' else f"-{sort_by}" queryset = queryset.order_by(ordering) return queryset - def get(self, request, *args, **kwargs): + def post(self, request, *args, **kwargs): review_querysets = EntityUploadStatus.get_user_entity_upload_status(request.user) review_querysets = self._search_queryset(review_querysets, self.request) review_querysets = self._filter_queryset(review_querysets, self.request) @@ -258,7 +258,7 @@ def fetch_criteria_values(self, criteria): if criteria == 'dataset': return self.fetch_dataset() return self.fetch_status() - + # breakpoint() filter_values = self.reviews_querysets.\ filter(**{f"{field}__isnull": False}).order_by().\ values_list(field, flat=True).distinct() diff --git a/django_project/dashboard/src/views/Review/List.tsx b/django_project/dashboard/src/views/Review/List.tsx index 1f6d8966..43e6fc72 100644 --- a/django_project/dashboard/src/views/Review/List.tsx +++ b/django_project/dashboard/src/views/Review/List.tsx @@ -1,24 +1,25 @@ -import React, {useEffect, useState} from "react"; +import React, {Fragment, useCallback, useEffect, useRef, useState} from "react"; import {useNavigate, useSearchParams} from "react-router-dom"; import axios from "axios"; import toLower from "lodash/toLower"; import {RootState} from "../../app/store"; -import { useAppSelector, useAppDispatch } from '../../app/hooks'; +import {useAppDispatch, useAppSelector} from '../../app/hooks'; import {setModule} from "../../reducers/module"; import {modules} from "../../modules"; -import List from "../../components/List"; +import {TABLE_OFFSET_HEIGHT} from "../../components/List"; +import ResizeTableEvent from "../../components/ResizeTableEvent"; import Loading from "../../components/Loading"; import {setSelectedReviews} from "../../reducers/reviewAction"; import MUIDataTable, {debounceSearchRender, MUISortOptions} from "mui-datatables"; import PaginationInterface, {getDefaultPagination, rowsPerPageOptions} from "../../models/pagination"; import {getDefaultFilter, ReviewFilterInterface} from "./Filter" +import {Button} from '@mui/material'; +import FilterAlt from "@mui/icons-material/FilterAlt"; import { setAvailableFilters, setCurrentColumns as setInitialColumns, setCurrentFilters as setInitialFilters } from "../../reducers/reviewTable"; -import {RootState} from "../../app/store"; - const USER_COLUMNS = [ 'id', @@ -46,7 +47,6 @@ interface reviewTableRowInterface { is_comparison_ready: string } - export interface ReviewData { id: number, revision: number, @@ -54,9 +54,11 @@ export interface ReviewData { } const FILTER_VALUES_API_URL = '/api/review-filter/values/' +const VIEW_LIST_URL = '/api/review-list/' +const FilterIcon: any = FilterAlt -export default function ReviewList () { +export default function ReviewList() { const initialColumns = useAppSelector((state: RootState) => state.reviewTable.currentColumns) const initialFilters = useAppSelector((state: RootState) => state.reviewTable.currentFilters) const availableFilters = useAppSelector((state: RootState) => state.reviewTable.availableFilters) @@ -76,6 +78,13 @@ export default function ReviewList () { const [pagination, setPagination] = useState(getDefaultPagination()) const [filterValues, setFilterValues] = useState(availableFilters) const [currentFilters, setCurrentFilters] = useState(initialFilters) + const axiosSource = useRef(null) + const newCancelToken = useCallback(() => { + axiosSource.current = axios.CancelToken.source(); + return axiosSource.current.token; + }, []) + const ref = useRef(null) + const [tableHeight, setTableHeight] = useState(0) const fetchFilterValues = async () => { let filters = [] @@ -114,17 +123,19 @@ export default function ReviewList () { } ).then((response) => { setLoading(false) - // update filter values if searchParams has upload filter - // let _upload = searchParams.get('upload') - // if (_upload) { - // setCustomOptions({ - // 'upload': { - // 'filterList': [_upload] - // } - // }) - setLoading(false) - setData(response.data.results as ReviewFilterInterface[]) - setTotalCount(response.data.count) + setData(response.data.results as ReviewFilterInterface[]) + setTotalCount(response.data.count) + }).catch(error => { + if (!axios.isCancel(error)) { + console.log(error) + setLoading(false) + if (error.response) { + if (error.response.status == 403) { + // TODO: use better way to handle 403 + navigate('/invalid_permission') + } + } + } }) } @@ -152,11 +163,10 @@ export default function ReviewList () { return values } - useEffect(() => { + useEffect(() => { const fetchFilterValuesData = async () => { - let filterVals: any = {} - if (filterValues.mode.length > 0 ) { + if (filterValues.status.length > 0) { filterVals = filterValues } else { filterVals = await fetchFilterValues() @@ -168,82 +178,26 @@ export default function ReviewList () { label: columnName.charAt(0).toUpperCase() + columnName.slice(1).replaceAll('_', ' '), options: { display: initialColumns.includes(columnName), - sort: !['tags', 'permissions', 'status', 'mode'].includes(columnName) - } - } - if (columnName == 'tags') { - _options['options']['filterType'] = 'multiselect' - _options['options']['filter'] = true - _options['options']['customBodyRender'] = (value: any, tableMeta: any) => { - return
- {value.map((tag: any, index: number) => )} -
+ sort: true } } - if (['tags', 'mode', 'dataset', 'is_default', 'min_privacy', 'max_privacy'].includes(columnName)) { + if (['level_0_entity', 'upload', 'revision', 'dataset', 'status'].includes(columnName)) { // set filter values in dropdown _options.options.filterOptions = { names: filterVals[columnName] } + _options.options.filterList = getExistingFilterValue(columnName) _options.options.filter = true } else { _options.options.filter = false } - if (['tags', 'mode', 'dataset', 'is_default', 'min_privacy', 'max_privacy'].includes(columnName)) { - // set existing filter values - _options.options.filterList = getExistingFilterValue(columnName) + if (columnName == 'start_date') { + _options.options.customBodyRender = (value: string) => { + return new Date(value).toDateString() + } } return _options }) - _columns.push({ - name: '', - options: { - customBodyRender: (value: any, tableMeta: any, updateValue: any) => { - let rowData = tableMeta.rowData - return ( -
- ) => { - event.stopPropagation(); - let obj: any = {} - USER_COLUMNS.forEach((element, index) => { - obj[element] = rowData[index]; - }); - setSelectedView(obj) - setAnchorEl(event.currentTarget); - }} - className='' - > - - - - { - setSelectedView(rowData) - setConfirmationText( - `Are you sure you want to delete ${rowData[1]}?`) - setConfirmationOpen(true) - }} - className='' - > - - -
- ) - }, - filter: false - } - }) setColumns(_columns) dispatch(setInitialColumns(JSON.stringify(_columns.map))) } @@ -346,20 +300,88 @@ export default function ReviewList () { loading ?
:
- + {/**/} + +
+ setTableHeight(0)} + onResize={(clientHeight: number) => setTableHeight(clientHeight - TABLE_OFFSET_HEIGHT)}/> +
+ { + return canRowBeSelected(dataIndex, selectedRows) + }, + onRowSelectionChange: (currentRowsSelected, allRowsSelected, rowsSelected) => { + // @ts-ignore + const rowDataSelected = rowsSelected.map((index) => rows[index]['id']) + selectionChanged(rowDataSelected) + }, + onRowClick: (rowData: string[], rowMeta: { dataIndex: number, rowIndex: number }) => { + handleRowClick(rowData, rowMeta) + }, + onTableChange: (action: string, tableState: any) => onTableChangeState(action, tableState), + customSearchRender: debounceSearchRender(500), + selectableRows: 'multiple', + textLabels: { + body: { + noMatch: loading ? + : + 'Sorry, there is no matching data to display', + }, + }, + onSearchChange: (searchText: string) => { + handleSearchOnChange(searchText) + }, + customFilterDialogFooter: (currentFilterList, applyNewFilters) => { + return ( +
+ +
+ ); + }, + onFilterChange: (column, filterList, type) => { + var newFilters = () => (filterList) + handleFilterSubmit(newFilters) + }, + searchText: currentFilters.search_text, + searchOpen: (currentFilters.search_text != null && currentFilters.search_text.length > 0), + filter: true, + filterType: 'multiselect', + confirmFilters: true + }} + components={{ + icons: { + FilterIcon + } + }} + /> +
+
+
) } From d0ea6d4265fe9845360a082ba90587882f235b51 Mon Sep 17 00:00:00 2001 From: Zakki Date: Wed, 2 Aug 2023 17:02:46 +0700 Subject: [PATCH 3/7] Add multiple select --- .../dashboard/src/views/Review/List.tsx | 51 ++++++------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/django_project/dashboard/src/views/Review/List.tsx b/django_project/dashboard/src/views/Review/List.tsx index 43e6fc72..d14034ba 100644 --- a/django_project/dashboard/src/views/Review/List.tsx +++ b/django_project/dashboard/src/views/Review/List.tsx @@ -1,20 +1,22 @@ import React, {Fragment, useCallback, useEffect, useRef, useState} from "react"; import {useNavigate, useSearchParams} from "react-router-dom"; + +import {Button} from '@mui/material'; +import FilterAlt from "@mui/icons-material/FilterAlt"; +import MUIDataTable, {debounceSearchRender, MUISortOptions} from "mui-datatables"; import axios from "axios"; import toLower from "lodash/toLower"; -import {RootState} from "../../app/store"; -import {useAppDispatch, useAppSelector} from '../../app/hooks'; -import {setModule} from "../../reducers/module"; -import {modules} from "../../modules"; -import {TABLE_OFFSET_HEIGHT} from "../../components/List"; -import ResizeTableEvent from "../../components/ResizeTableEvent"; + import Loading from "../../components/Loading"; -import {setSelectedReviews} from "../../reducers/reviewAction"; -import MUIDataTable, {debounceSearchRender, MUISortOptions} from "mui-datatables"; import PaginationInterface, {getDefaultPagination, rowsPerPageOptions} from "../../models/pagination"; +import ResizeTableEvent from "../../components/ResizeTableEvent"; +import {RootState} from "../../app/store"; +import {TABLE_OFFSET_HEIGHT} from "../../components/List"; import {getDefaultFilter, ReviewFilterInterface} from "./Filter" -import {Button} from '@mui/material'; -import FilterAlt from "@mui/icons-material/FilterAlt"; +import {modules} from "../../modules"; +import {setModule} from "../../reducers/module"; +import {setSelectedReviews} from "../../reducers/reviewAction"; +import {useAppDispatch, useAppSelector} from '../../app/hooks'; import { setAvailableFilters, setCurrentColumns as setInitialColumns, @@ -47,12 +49,6 @@ interface reviewTableRowInterface { is_comparison_ready: string } -export interface ReviewData { - id: number, - revision: number, - module: string -} - const FILTER_VALUES_API_URL = '/api/review-filter/values/' const VIEW_LIST_URL = '/api/review-list/' const FilterIcon: any = FilterAlt @@ -65,11 +61,9 @@ export default function ReviewList() { const [loading, setLoading] = useState(true) const [searchParams, setSearchParams] = useSearchParams() const [data, setData] = useState([]) - const [customOptions, setCustomOptions] = useState({}) const navigate = useNavigate() const dispatch = useAppDispatch() const isBatchReviewAvailable = useAppSelector((state: RootState) => state.reviewAction.isBatchReviewAvailable) - const isBatchReview = useAppSelector((state: RootState) => state.reviewAction.isBatchReview) const pendingReviews = useAppSelector((state: RootState) => state.reviewAction.pendingReviews) const reviewUpdatedAt = useAppSelector((state: RootState) => state.reviewAction.updatedAt) @@ -123,7 +117,7 @@ export default function ReviewList() { } ).then((response) => { setLoading(false) - setData(response.data.results as ReviewFilterInterface[]) + setData(response.data.results as reviewTableRowInterface[]) setTotalCount(response.data.count) }).catch(error => { if (!axios.isCancel(error)) { @@ -300,20 +294,6 @@ export default function ReviewList() { loading ?
:
- {/**/}
setTableHeight(0)} @@ -332,11 +312,11 @@ export default function ReviewList() { sortOrder: pagination.sortOrder as MUISortOptions, jumpToPage: true, isRowSelectable: (dataIndex: number, selectedRows: any) => { - return canRowBeSelected(dataIndex, selectedRows) + return canRowBeSelected(dataIndex, data[dataIndex]) }, onRowSelectionChange: (currentRowsSelected, allRowsSelected, rowsSelected) => { // @ts-ignore - const rowDataSelected = rowsSelected.map((index) => rows[index]['id']) + const rowDataSelected = rowsSelected.map((index) => data[index]['id']) selectionChanged(rowDataSelected) }, onRowClick: (rowData: string[], rowMeta: { dataIndex: number, rowIndex: number }) => { @@ -345,6 +325,7 @@ export default function ReviewList() { onTableChange: (action: string, tableState: any) => onTableChangeState(action, tableState), customSearchRender: debounceSearchRender(500), selectableRows: 'multiple', + selectToolbarPlacement: 'none', textLabels: { body: { noMatch: loading ? From 342d85a5bb0564a8f84846fbf0696a98fa320eeb Mon Sep 17 00:00:00 2001 From: Zakki Date: Wed, 2 Aug 2023 19:50:34 +0700 Subject: [PATCH 4/7] Fix hide/show Review row selector --- django_project/dashboard/src/views/Review/List.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/django_project/dashboard/src/views/Review/List.tsx b/django_project/dashboard/src/views/Review/List.tsx index d14034ba..895f96c0 100644 --- a/django_project/dashboard/src/views/Review/List.tsx +++ b/django_project/dashboard/src/views/Review/List.tsx @@ -63,6 +63,7 @@ export default function ReviewList() { const [data, setData] = useState([]) const navigate = useNavigate() const dispatch = useAppDispatch() + const isBatchReview = useAppSelector((state: RootState) => state.reviewAction.isBatchReview) const isBatchReviewAvailable = useAppSelector((state: RootState) => state.reviewAction.isBatchReviewAvailable) const pendingReviews = useAppSelector((state: RootState) => state.reviewAction.pendingReviews) const reviewUpdatedAt = useAppSelector((state: RootState) => state.reviewAction.updatedAt) @@ -80,6 +81,8 @@ export default function ReviewList() { const ref = useRef(null) const [tableHeight, setTableHeight] = useState(0) + let selectableRowsMode: any = isBatchReview ? 'multiple' : 'none' + const fetchFilterValues = async () => { let filters = [] filters.push(axios.get(`${FILTER_VALUES_API_URL}level_0_entity/`)) @@ -324,7 +327,7 @@ export default function ReviewList() { }, onTableChange: (action: string, tableState: any) => onTableChangeState(action, tableState), customSearchRender: debounceSearchRender(500), - selectableRows: 'multiple', + selectableRows: selectableRowsMode, selectToolbarPlacement: 'none', textLabels: { body: { From 02e18628cb3441ed31db160ac17b1d9861d92f5f Mon Sep 17 00:00:00 2001 From: Zakki Date: Wed, 2 Aug 2023 19:50:44 +0700 Subject: [PATCH 5/7] Add Review List Test --- .../dashboard/tests/test_review_list.py | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 django_project/dashboard/tests/test_review_list.py diff --git a/django_project/dashboard/tests/test_review_list.py b/django_project/dashboard/tests/test_review_list.py new file mode 100644 index 00000000..ec9f9c91 --- /dev/null +++ b/django_project/dashboard/tests/test_review_list.py @@ -0,0 +1,178 @@ +__author__ = 'zakki@kartoza.com' +__date__ = '31/07/23' +__copyright__ = ('Copyright 2023, Unicef') + +import urllib.parse + +from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APIRequestFactory + +from dashboard.api_views.reviews import ReviewList +from dashboard.models.entity_upload import REVIEWING, REJECTED +from dashboard.tests.model_factories import ( + EntityUploadF, LayerUploadSessionF +) +from georepo.tests.model_factories import ( + UserF, DatasetF, + ModuleF +) + + +class TestReviewList(TestCase): + + def setUp(self) -> None: + self.factory = APIRequestFactory() + self.superuser = UserF.create(is_superuser=True) + self.creator = UserF.create() + self.module = ModuleF.create( + name='Admin Boundaries' + ) + dataset = DatasetF.create( + module=self.module, + generate_adm0_default_views=True + ) + self.upload_session = LayerUploadSessionF.create( + dataset=dataset, + uploader=self.creator + ) + self.entity_upload_status_1 = EntityUploadF.create( + upload_session=self.upload_session, + status=REVIEWING + ) + EntityUploadF.create( + upload_session=self.upload_session, + status=REJECTED + ) + self.entity_upload_status_3 = EntityUploadF.create( + upload_session=self.upload_session, + status=REVIEWING + ) + + def test_list_views(self): + """ + Test Review List without any parameter. + It will return only entity_upload_status_1, as only those with status + REVIEWING or APPROVED will be returned. + """ + request = self.factory.post( + reverse('review-list') + ) + request.user = self.superuser + list_view = ReviewList.as_view() + response = list_view(request) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['count'], 2) + self.assertEqual(response.data['page'], 1) + self.assertEqual(response.data['total_page'], 1) + self.assertEqual( + response.data['results'][0].get('id'), + self.entity_upload_status_1.id + ) + self.assertEqual( + response.data['results'][1].get('id'), + self.entity_upload_status_3.id + ) + + request = self.factory.post( + reverse('view-list') + ) + + def test_sort(self): + """ + Test sorting Review List by ID descending. + """ + query_params = { + 'sort_by': 'id', + 'sort_direction': 'desc' + } + request = self.factory.post( + f"{reverse('review-list')}?{urllib.parse.urlencode(query_params)}" + ) + request.user = self.superuser + list_view = ReviewList.as_view() + response = list_view(request) + self.assertEqual(response.data['count'], 2) + self.assertEqual(response.data['page'], 1) + self.assertEqual(response.data['total_page'], 1) + self.assertEqual( + response.data['results'][0].get('id'), + self.entity_upload_status_3.id + ) + self.assertEqual( + response.data['results'][1].get('id'), + self.entity_upload_status_1.id + ) + + def test_pagination(self): + """ + Test Review List pagination. + """ + query_params = { + 'page': 2, + 'page_size': 1 + } + request = self.factory.post( + f"{reverse('review-list')}?{urllib.parse.urlencode(query_params)}" + ) + request.user = self.superuser + list_view = ReviewList.as_view() + response = list_view(request) + self.assertEqual(response.data['page'], 2) + self.assertEqual(response.data['total_page'], 2) + self.assertEqual(len(response.data['results']), 1) + self.assertEqual( + response.data['results'][0].get('id'), + self.entity_upload_status_3.id + ) + + def test_search(self): + """ + Test Review List search. + """ + entity_upload_status_3 = EntityUploadF.create( + upload_session=self.upload_session, + status=REVIEWING, + logs='Some logs' + ) + request = self.factory.post( + reverse('review-list'), + { + 'search_text': entity_upload_status_3.logs + } + ) + request.user = self.superuser + list_view = ReviewList.as_view() + response = list_view(request) + self.assertEqual(response.data['count'], 1) + self.assertEqual(response.data['page'], 1) + self.assertEqual(response.data['total_page'], 1) + self.assertEqual( + response.data['results'][0].get('id'), + entity_upload_status_3.id + ) + + def test_filter(self): + """ + Test Review List filter. + """ + request = self.factory.post( + reverse('review-list'), + { + 'dataset': [self.entity_upload_status_3.upload_session.dataset.label] + } + ) + request.user = self.superuser + list_view = ReviewList.as_view() + response = list_view(request) + self.assertEqual(response.data['count'], 2) + self.assertEqual(response.data['page'], 1) + self.assertEqual(response.data['total_page'], 1) + self.assertEqual( + response.data['results'][0].get('id'), + self.entity_upload_status_1.id + ) + self.assertEqual( + response.data['results'][1].get('id'), + self.entity_upload_status_3.id + ) From c0149ce136277ae307f9eedaaffd00bc391c3e3d Mon Sep 17 00:00:00 2001 From: Zakki Date: Wed, 2 Aug 2023 22:00:59 +0700 Subject: [PATCH 6/7] Add test for review filter value and fix failed test. --- django_project/dashboard/api_views/reviews.py | 24 +--- .../dashboard/tests/test_api_views.py | 10 +- .../tests/test_review_filter_value.py | 135 ++++++++++++++++++ .../dashboard/tests/test_review_list.py | 2 +- 4 files changed, 144 insertions(+), 27 deletions(-) create mode 100644 django_project/dashboard/tests/test_review_filter_value.py diff --git a/django_project/dashboard/api_views/reviews.py b/django_project/dashboard/api_views/reviews.py index 222690f2..409be1ce 100644 --- a/django_project/dashboard/api_views/reviews.py +++ b/django_project/dashboard/api_views/reviews.py @@ -258,36 +258,18 @@ def fetch_criteria_values(self, criteria): if criteria == 'dataset': return self.fetch_dataset() return self.fetch_status() - # breakpoint() + filter_values = self.reviews_querysets.\ filter(**{f"{field}__isnull": False}).order_by().\ values_list(field, flat=True).distinct() return [val for val in filter_values] - def fetch_level_0_entity(self): - filter_values = self.reviews_querysets.\ - filter(revised_geographical_entity__label__isnull=False).order_by().\ - values_list('revised_geographical_entity__label', flat=True).distinct() - return [val for val in filter_values] - - def fetch_upload(self): - filter_values = self.reviews_querysets. \ - filter(upload_session__source__isnull=False).order_by(). \ - values_list('upload_session__source', flat=True).distinct() - return [val for val in filter_values] - def fetch_dataset(self): - return self.reviews_querysets.filter( + return list(self.reviews_querysets.filter( upload_session__dataset__label__isnull=False ).exclude( upload_session__dataset__label__exact='' - ).order_by().values_list('upload_session__dataset__label', flat=True).distinct() - - def fetch_revision(self): - filter_values = self.reviews_querysets. \ - filter(revised_geographical_entity__revision_number__isnull=False).order_by(). \ - values_list('revised_geographical_entity__revision_number', flat=True).distinct() - return [val for val in filter_values] + ).order_by().values_list('upload_session__dataset__label', flat=True).distinct()) def fetch_status(self): return [ diff --git a/django_project/dashboard/tests/test_api_views.py b/django_project/dashboard/tests/test_api_views.py index 422b311f..3964c906 100644 --- a/django_project/dashboard/tests/test_api_views.py +++ b/django_project/dashboard/tests/test_api_views.py @@ -514,14 +514,14 @@ def test_get_review_list(self): upload_session=upload_session ) - request = self.factory.get( + request = self.factory.post( reverse('review-list') ) request.user = user view = ReviewList.as_view() response = view(request) self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data), 1) + self.assertEqual(len(response.data['results']), 1) def test_update_layer_upload(self): updated_by = UserF.create(username='test_user') @@ -612,15 +612,15 @@ def test_send_to_ready_reviews(self): LayerUploadSession.objects.get(id=upload_session.id).status, 'Reviewing' ) - request = self.factory.get( + request = self.factory.post( reverse('review-list') ) request.user = self.superuser view = ReviewList.as_view() response = view(request) self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data), 2) - self.assertEqual(response.data[0]['submitted_by'], user.username) + self.assertEqual(len(response.data['results']), 2) + self.assertEqual(response.data['results'][0]['submitted_by'], user.username) # simulate when there is other upload in review of same entity upload_session_2 = LayerUploadSessionF.create( dataset=upload_session.dataset diff --git a/django_project/dashboard/tests/test_review_filter_value.py b/django_project/dashboard/tests/test_review_filter_value.py new file mode 100644 index 00000000..607f3506 --- /dev/null +++ b/django_project/dashboard/tests/test_review_filter_value.py @@ -0,0 +1,135 @@ +__author__ = 'zakki@kartoza.com' +__date__ = '02/08/23' +__copyright__ = ('Copyright 2023, Unicef') + +import urllib.parse +import json + +from django.test import TestCase +from django.urls import reverse +from rest_framework.test import APIRequestFactory +from django.contrib.gis.geos import GEOSGeometry + +from dashboard.api_views.reviews import ReviewFilterValue +from dashboard.models.entity_upload import APPROVED, REVIEWING, REJECTED +from dashboard.tests.model_factories import ( + EntityUploadF, LayerUploadSessionF +) +from georepo.tests.model_factories import ( + UserF, DatasetF, + ModuleF, GeographicalEntityF +) +from georepo.utils import absolute_path + + +class TestReviewFilterValue(TestCase): + + def setUp(self) -> None: + self.factory = APIRequestFactory() + self.superuser = UserF.create(is_superuser=True) + self.creator = UserF.create() + self.module = ModuleF.create( + name='Admin Boundaries' + ) + dataset = DatasetF.create( + module=self.module, + generate_adm0_default_views=True + ) + self.upload_session = LayerUploadSessionF.create( + dataset=dataset, + uploader=self.creator, + source='TEST' + ) + geojson_0_path = absolute_path( + 'georepo', 'tests', + 'geojson_dataset', 'level_0.geojson') + with open(geojson_0_path) as geojson: + data = json.load(geojson) + geom_str = json.dumps(data['features'][0]['geometry']) + self.entity_1 = GeographicalEntityF.create( + revision_number=1, + level=0, + dataset=dataset, + geometry=GEOSGeometry(geom_str), + internal_code='PAK', + label='Pakistan' + ) + self.entity_2 = GeographicalEntityF.create( + revision_number=2, + level=1, + dataset=dataset, + geometry=GEOSGeometry(geom_str), + internal_code='PAK01', + label='Islamabad' + ) + self.entity_upload_status_1 = EntityUploadF.create( + upload_session=self.upload_session, + status=REVIEWING, + revised_geographical_entity=self.entity_1 + ) + EntityUploadF.create( + upload_session=self.upload_session, + status=REJECTED, + ) + self.entity_upload_status_3 = EntityUploadF.create( + upload_session=self.upload_session, + status=APPROVED, + revised_geographical_entity=self.entity_2 + ) + + def test_list_level_0_entity(self): + request = self.factory.get( + reverse('review-filter-value', kwargs={'criteria': 'level_0_entity'}) + ) + request.user = self.superuser + list_view = ReviewFilterValue.as_view() + response = list_view(request, 'level_0_entity') + self.assertEquals( + response.data, + [ + self.entity_2.label, + self.entity_1.label, + ] + ) + + def test_list_upload(self): + request = self.factory.get( + reverse('review-filter-value', kwargs={'criteria': 'upload'}) + ) + request.user = self.superuser + list_view = ReviewFilterValue.as_view() + response = list_view(request, 'upload') + self.assertEquals(response.data, ['TEST']) + + def test_list_revision(self): + request = self.factory.get( + reverse('review-filter-value', kwargs={'criteria': 'revision'}) + ) + request.user = self.superuser + list_view = ReviewFilterValue.as_view() + response = list_view(request, 'revision') + self.assertEquals( + response.data, + [ + self.entity_1.revision_number, + self.entity_2.revision_number + ] + ) + + def test_list_dataset(self): + request = self.factory.get( + reverse('review-filter-value', kwargs={'criteria': 'dataset'}) + ) + request.user = self.superuser + list_view = ReviewFilterValue.as_view() + response = list_view(request, 'dataset') + self.assertEquals(response.data, [self.upload_session.dataset.label]) + + def test_list_status(self): + request = self.factory.get( + reverse('review-filter-value', kwargs={'criteria': 'status'}) + ) + request.user = self.superuser + list_view = ReviewFilterValue.as_view() + response = list_view(request, 'status') + self.assertEquals(response.data, [APPROVED, REJECTED, 'Pending']) \ No newline at end of file diff --git a/django_project/dashboard/tests/test_review_list.py b/django_project/dashboard/tests/test_review_list.py index ec9f9c91..abde2007 100644 --- a/django_project/dashboard/tests/test_review_list.py +++ b/django_project/dashboard/tests/test_review_list.py @@ -1,5 +1,5 @@ __author__ = 'zakki@kartoza.com' -__date__ = '31/07/23' +__date__ = '02/08/23' __copyright__ = ('Copyright 2023, Unicef') import urllib.parse From 551eaab0f3c4ee32cf9b853ac0fb54b0d5cc5890 Mon Sep 17 00:00:00 2001 From: Zakki Date: Wed, 2 Aug 2023 22:06:51 +0700 Subject: [PATCH 7/7] Address flake8 feedback --- django_project/dashboard/api_views/reviews.py | 38 +++++++++++++++---- .../dashboard/tests/test_api_views.py | 5 ++- .../tests/test_review_filter_value.py | 11 ++++-- .../dashboard/tests/test_review_list.py | 4 +- 4 files changed, 45 insertions(+), 13 deletions(-) diff --git a/django_project/dashboard/api_views/reviews.py b/django_project/dashboard/api_views/reviews.py index 409be1ce..c96632cb 100644 --- a/django_project/dashboard/api_views/reviews.py +++ b/django_project/dashboard/api_views/reviews.py @@ -157,7 +157,8 @@ def _filter_queryset(self, queryset, request): if 'status' in dict(request.data): filter_values = sorted(dict(request.data).get('status', [])) - if not filter_values or filter_values == [APPROVED, 'Pending', REJECTED]: + if not filter_values or \ + filter_values == [APPROVED, 'Pending', REJECTED]: return queryset.filter(**filter_kwargs) non_pending_filter_combinations = [ @@ -173,9 +174,21 @@ def _filter_queryset(self, queryset, request): filter_kwargs.update({'status__in': filter_values}) elif 'Pending' in filter_values: if APPROVED in filter_values: - filter_kwargs.update({'status__in': [*pending_status, APPROVED]}) + filter_kwargs.update( + { + 'status__in': [ + *pending_status, APPROVED + ] + } + ) elif REJECTED in filter_values: - filter_kwargs.update({'status__in': [*pending_status, REJECTED]}) + filter_kwargs.update( + { + 'status__in': [ + *pending_status, REJECTED + ] + } + ) else: filter_kwargs.update({'status__in': [pending_status]}) @@ -211,9 +224,15 @@ def _sort_queryset(self, queryset, request): return queryset def post(self, request, *args, **kwargs): - review_querysets = EntityUploadStatus.get_user_entity_upload_status(request.user) - review_querysets = self._search_queryset(review_querysets, self.request) - review_querysets = self._filter_queryset(review_querysets, self.request) + review_querysets = EntityUploadStatus.get_user_entity_upload_status( + request.user + ) + review_querysets = self._search_queryset( + review_querysets, self.request + ) + review_querysets = self._filter_queryset( + review_querysets, self.request + ) page = int(self.request.GET.get('page', '1')) page_size = int(self.request.query_params.get('page_size', '10')) review_querysets = self._sort_queryset(review_querysets, self.request) @@ -269,7 +288,9 @@ def fetch_dataset(self): upload_session__dataset__label__isnull=False ).exclude( upload_session__dataset__label__exact='' - ).order_by().values_list('upload_session__dataset__label', flat=True).distinct()) + ).order_by().values_list( + 'upload_session__dataset__label', flat=True + ).distinct()) def fetch_status(self): return [ @@ -279,7 +300,8 @@ def fetch_status(self): ] def get(self, request, criteria, *args, **kwargs): - self.reviews_querysets = EntityUploadStatus.get_user_entity_upload_status(request.user) + self.reviews_querysets = \ + EntityUploadStatus.get_user_entity_upload_status(request.user) try: data = self.fetch_criteria_values(criteria) except AttributeError: diff --git a/django_project/dashboard/tests/test_api_views.py b/django_project/dashboard/tests/test_api_views.py index 3964c906..c237830e 100644 --- a/django_project/dashboard/tests/test_api_views.py +++ b/django_project/dashboard/tests/test_api_views.py @@ -620,7 +620,10 @@ def test_send_to_ready_reviews(self): response = view(request) self.assertEqual(response.status_code, 200) self.assertEqual(len(response.data['results']), 2) - self.assertEqual(response.data['results'][0]['submitted_by'], user.username) + self.assertEqual( + response.data['results'][0]['submitted_by'], + user.username + ) # simulate when there is other upload in review of same entity upload_session_2 = LayerUploadSessionF.create( dataset=upload_session.dataset diff --git a/django_project/dashboard/tests/test_review_filter_value.py b/django_project/dashboard/tests/test_review_filter_value.py index 607f3506..91e9b904 100644 --- a/django_project/dashboard/tests/test_review_filter_value.py +++ b/django_project/dashboard/tests/test_review_filter_value.py @@ -2,7 +2,6 @@ __date__ = '02/08/23' __copyright__ = ('Copyright 2023, Unicef') -import urllib.parse import json from django.test import TestCase @@ -79,7 +78,10 @@ def setUp(self) -> None: def test_list_level_0_entity(self): request = self.factory.get( - reverse('review-filter-value', kwargs={'criteria': 'level_0_entity'}) + reverse( + 'review-filter-value', + kwargs={'criteria': 'level_0_entity'} + ) ) request.user = self.superuser list_view = ReviewFilterValue.as_view() @@ -132,4 +134,7 @@ def test_list_status(self): request.user = self.superuser list_view = ReviewFilterValue.as_view() response = list_view(request, 'status') - self.assertEquals(response.data, [APPROVED, REJECTED, 'Pending']) \ No newline at end of file + self.assertEquals( + response.data, + [APPROVED, REJECTED, 'Pending'] + ) diff --git a/django_project/dashboard/tests/test_review_list.py b/django_project/dashboard/tests/test_review_list.py index abde2007..a4fb02e3 100644 --- a/django_project/dashboard/tests/test_review_list.py +++ b/django_project/dashboard/tests/test_review_list.py @@ -159,7 +159,9 @@ def test_filter(self): request = self.factory.post( reverse('review-list'), { - 'dataset': [self.entity_upload_status_3.upload_session.dataset.label] + 'dataset': [ + self.entity_upload_status_3.upload_session.dataset.label + ] } ) request.user = self.superuser