Skip to content

Commit

Permalink
Merge pull request #6 from unicef-drp/feature/825-Review-Listing-Serv…
Browse files Browse the repository at this point in the history
…er-Side

Feature/825 review listing server side
  • Loading branch information
danangmassandy authored Aug 3, 2023
2 parents 7b02999 + 96d674f commit 11e5968
Show file tree
Hide file tree
Showing 10 changed files with 907 additions and 63 deletions.
199 changes: 176 additions & 23 deletions django_project/dashboard/api_views/reviews.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -137,23 +140,173 @@ class ReviewList(AzureAuthRequiredMixin, APIView):
"""Api to list all ready to review uploads"""
permission_classes = [IsAuthenticated]

def get(self, request, *args, **kwargs):
datasets = Dataset.objects.all().order_by('created_at')
datasets = get_dataset_to_review(
self.request.user,
datasets
def _filter_queryset(self, queryset, request):
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 queryset.filter(**filter_kwargs)

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._meta.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', 'id')
sort_direction = request.query_params.get('sort_direction', 'asc')
if not sort_by:
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 post(self, request, *args, **kwargs):
review_querysets = EntityUploadStatus.get_user_entity_upload_status(
request.user
)
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 = 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_dataset(self):
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_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,
Expand Down
19 changes: 19 additions & 0 deletions django_project/dashboard/models/entity_upload.py
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 2 additions & 0 deletions django_project/dashboard/src/app/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
import viewTableReducer from "../reducers/viewTable"

export const store = configureStore({
Expand All @@ -15,6 +16,7 @@ export const store = configureStore({
pollInterval: pollIntervalReducer,
maintenanceItem: maintenanceReducer,
reviewAction: reviewActionReducer,
reviewTable: reviewTableReducer,
viewTable: viewTableReducer
},
});
Expand Down
52 changes: 52 additions & 0 deletions django_project/dashboard/src/reducers/reviewTable.ts
Original file line number Diff line number Diff line change
@@ -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<string>) => {
state.currentColumns = JSON.parse(action.payload)
},
setCurrentFilters: (state, action: PayloadAction<string>) => {
state.currentFilters = JSON.parse(action.payload)
},
setAvailableFilters: (state, action: PayloadAction<string>) => {
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
19 changes: 19 additions & 0 deletions django_project/dashboard/src/views/Review/Filter.ts
Original file line number Diff line number Diff line change
@@ -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: ''
}
}
Loading

1 comment on commit 11e5968

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report for django_project/dashboard

St.
Category Percentage Covered / Total
🔴 Statements 5.88% 406/6907
🔴 Branches 0.91% 34/3744
🔴 Functions 2.95% 52/1762
🔴 Lines 5.93% 401/6760

Test suite run success

12 tests passing in 5 suites.

Report generated by 🧪jest coverage report action from 11e5968

Please sign in to comment.