Skip to content

Commit

Permalink
Add navigation action to ProjectViewSet and refactor navigation (#299,
Browse files Browse the repository at this point in the history
  • Loading branch information
jochenklar committed Oct 10, 2023
1 parent 3be5047 commit 464977d
Show file tree
Hide file tree
Showing 7 changed files with 125 additions and 89 deletions.
95 changes: 78 additions & 17 deletions rdmo/projects/progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
from rdmo.questions.models import Catalog, Section, Page, QuestionSet, Question


def compute_progress(project, snapshot=None):
# get all values for this project and snapshot
project_values = project.values.filter(snapshot=snapshot).select_related('attribute', 'option')

def resolve_conditions(project, values):
# get all conditions for this catalog
pages_conditions_subquery = Page.objects.filter_by_catalog(project.catalog).filter(conditions=OuterRef('pk'))
questionsets_conditions_subquery = QuestionSet.objects.filter_by_catalog(project.catalog).filter(conditions=OuterRef('pk'))
Expand All @@ -24,24 +21,88 @@ def compute_progress(project, snapshot=None):
# evaluate conditions
conditions = set()
for condition in catalog_conditions:
if condition.resolve(project_values):
if condition.resolve(values):
conditions.add(condition.id)

# compute sets from values
# return all true conditions for this project
return conditions


def compute_sets(values):
sets = defaultdict(list)
for attribute, set_index in project_values.values_list('attribute', 'set_index').distinct():
for attribute, set_index in values.values_list('attribute', 'set_index').distinct():
sets[attribute].append(set_index)
return sets


def compute_navigation(section, project, snapshot=None):
# get all values for this project and snapshot
values = project.values.filter(snapshot=snapshot).select_related('attribute', 'option')

# get true conditions
conditions = resolve_conditions(project, values)

# compute sets from values
sets = compute_sets(values)

# query non empty values
values_list = values.exclude((Q(text='') | Q(text=None)) & Q(option=None) &
(Q(file='') | Q(file=None))) \
.values_list('attribute', 'set_index').distinct() \
.values_list('attribute', flat=True)

navigation = []
for catalog_section in project.catalog.elements:
navigation_section = {
'id': catalog_section.id,
'title': catalog_section.title,
'first': catalog_section.elements[0].id if section.elements else None
}
if catalog_section.id == section.id:
navigation_section['pages'] = []
for page in catalog_section.elements:
pages_conditions = set(page.id for page in page.conditions.all())
show = bool(not pages_conditions or pages_conditions.intersection(conditions))

# count the total number of questions, taking sets and conditions into account
total, attributes = count_questions(page, sets, conditions)

# filter the project values for the counted questions and exclude empty values
count = len(tuple(filter(lambda attribute: attribute in attributes, values_list)))

navigation_section['pages'].append({
'id': page.id,
'title': page.title,
'show': show,
'count': count,
'total': total
})

navigation.append(navigation_section)

return navigation


def compute_progress(project, snapshot=None):
# get all values for this project and snapshot
values = project.values.filter(snapshot=snapshot).select_related('attribute', 'option')

# get true conditions
conditions = resolve_conditions(project, values)

# compute sets from values
sets = compute_sets(values)

# count the total number of questions, taking sets and conditions into account
total_count, attributes = count_questions(project.catalog, sets, conditions)
total, attributes = count_questions(project.catalog, sets, conditions)

# filter the project values for the counted questions and exclude empty values
values_count = project_values.filter(attribute__in=attributes) \
.exclude((Q(text='') | Q(text=None)) & Q(option=None) &
(Q(file='') | Q(file=None))) \
.count()
count = values.filter(attribute_id__in=attributes) \
.exclude((Q(text='') | Q(text=None)) & Q(option=None) &
(Q(file='') | Q(file=None))) \
.values_list('attribute', 'set_index').distinct().count()

return values_count, total_count
return count, total


def count_questions(parent_element, sets, conditions):
Expand All @@ -58,11 +119,11 @@ def count_questions(parent_element, sets, conditions):
if not element_conditions or element_conditions.intersection(conditions):
if isinstance(element, Question):
if not element.is_optional:
attributes.append(element.attribute)
attributes.append(element.attribute_id)
count += 1
else:
if element.attribute:
attributes.append(element.attribute)
if element.attribute_id:
attributes.append(element.attribute_id)

element_count, element_attributes = count_questions(element, sets, conditions)
set_count = count_sets(element, sets)
Expand All @@ -75,7 +136,7 @@ def count_questions(parent_element, sets, conditions):

def count_sets(parent_element, sets):
if parent_element.is_collection:
if parent_element.attribute:
if parent_element.attribute_id:
count = len(sets[parent_element.attribute_id])
else:
count = 0
Expand Down
37 changes: 2 additions & 35 deletions rdmo/projects/serializers/v1/overview.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,18 @@
from rest_framework import serializers

from rdmo.projects.models import Project
from rdmo.questions.models import Catalog, Page, Section


class PageSerializer(serializers.ModelSerializer):

class Meta:
model = Page
fields = (
'id',
'title',
'has_conditions'
)


class SectionSerializer(serializers.ModelSerializer):

pages = serializers.SerializerMethodField()

class Meta:
model = Section
fields = (
'id',
'title',
'pages'
)

def get_pages(self, obj):
return PageSerializer(obj.elements, many=True, read_only=True).data
from rdmo.questions.models import Catalog


class CatalogSerializer(serializers.ModelSerializer):

sections = serializers.SerializerMethodField()

class Meta:
model = Catalog
fields = (
'id',
'title',
'sections'
'title'
)

def get_sections(self, obj):
return SectionSerializer(obj.elements, many=True, read_only=True).data


class ProjectOverviewSerializer(serializers.ModelSerializer):

Expand Down
1 change: 1 addition & 0 deletions rdmo/projects/serializers/v1/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ def get_section(self, obj):
return {
'id': section.id,
'title': section.title,
'first': section.elements[0].id if section.elements else None
} if section else {}

def get_prev_page(self, obj):
Expand Down
37 changes: 15 additions & 22 deletions rdmo/projects/static/projects/js/project_questions/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ angular.module('project_questions')
/* configure resources */

var resources = {
projects: $resource(baseurl + 'api/v1/projects/projects/:id/:detail_action/'),
projects: $resource(baseurl + 'api/v1/projects/projects/:id/:detail_action/:detail_id/'),
values: $resource(baseurl + 'api/v1/projects/projects/:project/values/:id/:detail_action/'),
pages: $resource(baseurl + 'api/v1/projects/projects/:project/pages/:list_action/:id/'),
settings: $resource(baseurl + 'api/v1/core/settings/')
Expand Down Expand Up @@ -147,13 +147,14 @@ angular.module('project_questions')
}

return service.fetchPage(page_id)
.then(service.fetchNavigation)
.then(service.fetchOptions)
.then(service.fetchValues)
.then(service.fetchConditions)
.then(function () {
// copy future objects
angular.forEach([
'page', 'progress', 'attributes', 'questionsets', 'questions', 'valuesets', 'values'
'page', 'progress', 'attributes', 'questionsets', 'questions', 'valuesets', 'values', 'navigation'
], function (key) {
service[key] = angular.copy(future[key]);
});
Expand Down Expand Up @@ -276,6 +277,16 @@ angular.module('project_questions')
});
};

service.fetchNavigation = function() {
future.navigation = resources.projects.query({
id: service.project.id,
detail_id: future.page.section.id,
detail_action: 'navigation'
});

return future.navigation.$promise
};

service.initPage = function(page) {
// store attributes in a separate array
if (page.attribute !== null) future.attributes.push(page.attribute);
Expand Down Expand Up @@ -882,16 +893,7 @@ angular.module('project_questions')
} else if (angular.isDefined(page)) {
service.initView(page.id);
} else if (angular.isDefined(section)) {
if (angular.isDefined(section.pages)) {
service.initView(section.pages[0].id);
} else {
// jump to first page of the section in breadcrumb
// let section_from_service = service.project.catalog.sections.find(x => x.id === section.id)
var section_from_service = $filter('filter')(service.project.catalog.sections, {
id: section.id
})[0]
service.initView(section_from_service.pages[0].id);
}
service.initView(section.first);
} else {
service.initView(null);
}
Expand All @@ -900,16 +902,7 @@ angular.module('project_questions')
if (angular.isDefined(page)) {
service.initView(page.id);
} else if (angular.isDefined(section)) {
if (angular.isDefined(section.pages)) {
service.initView(section.pages[0].id);
} else {
// jump to first page of the section in breadcrumb
// let section_from_service = service.project.catalog.sections.find(x => x.id === section.id)
var section_from_service = $filter('filter')(service.project.catalog.sections, {
id: section.id
})[0]
service.initView(section_from_service.pages[0].id);
}
service.initView(section.first);
} else {
service.initView(null);
}
Expand Down
15 changes: 10 additions & 5 deletions rdmo/projects/templates/projects/project_questions_navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,27 @@
{% include 'projects/project_questions_navigation_help.html' %}

<ul class="list-unstyled project-questions-overview">
<li ng-repeat="section in service.project.catalog.sections">
<li ng-repeat="section in service.navigation">
<a href="" ng-click="service.jump(section)">
{$ section.title $}
</a>

<ul class="list-unstyled"
ng-show="section.id == service.page.section.id">
ng-show="section.pages">
<li ng-repeat="page in section.pages"
ng-class="{'active': page.id == service.page.id}">

<a href="" ng-click="service.jump(section, page)">

<a href="" ng-click="service.jump(section, page)" ng-show="page.show">
<span>{$ page.title $}<span>
<span ng-show="page.has_conditions" class="small">
<i class="fa fa-question-circle-o small" aria-hidden="true"></i>
<span ng-show="page.count == page.total">
<i class="fa fa-check" aria-hidden="true"></i>
</span>
<span ng-show="page.count > 0 && page.count != page.total">
({$ page.count $} of {$ page.total $})
<span>
</a>
<span class="text-muted" ng-hide="page.show">{$ page.title $}<span>
</li>
</ul>
</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@

<p class="help-block">
{% blocktrans trimmed %}
Entries with <i class="fa fa-question-circle-o small" aria-hidden="true"></i> might be skipped based on your input.
Grey entries will be conditionally skipped based on your input.
{% endblocktrans %}
</p>
27 changes: 18 additions & 9 deletions rdmo/projects/viewsets.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
from django.conf import settings
from django.contrib.sites.shortcuts import get_current_site
from django.db.models import prefetch_related_objects
from django.core.exceptions import ObjectDoesNotExist
from django.http import Http404, HttpResponseRedirect
from django.utils.translation import gettext_lazy as _

from rest_framework import serializers
from rest_framework.decorators import action
from rest_framework.exceptions import NotFound
from rest_framework.mixins import CreateModelMixin, ListModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.viewsets import GenericViewSet, ModelViewSet, ReadOnlyModelViewSet
Expand All @@ -27,7 +26,7 @@
from .filters import SnapshotFilterBackend, ValueFilterBackend
from .models import Continuation, Integration, Invite, Issue, Membership, Project, Snapshot, Value
from .permissions import HasProjectPagePermission, HasProjectPermission, HasProjectsPermission
from .progress import compute_progress
from .progress import compute_navigation, compute_progress
from .serializers.v1 import (
IntegrationSerializer,
InviteSerializer,
Expand Down Expand Up @@ -66,17 +65,27 @@ class ProjectViewSet(ModelViewSet):
def get_queryset(self):
return Project.objects.filter_user(self.request.user).select_related('catalog')

@action(detail=True, permission_classes=(IsAuthenticated, ))
@action(detail=True, permission_classes=(HasModelPermission | HasProjectPermission, ))
def overview(self, request, pk=None):
project = self.get_object()

# prefetch only the pages (and their conditions)
prefetch_related_objects([project.catalog],
'catalog_sections__section__section_pages__page__conditions')

serializer = ProjectOverviewSerializer(project, context={'request': request})
return Response(serializer.data)

@action(detail=True, url_path=r'navigation/(?P<section_id>\d+)',
permission_classes=(HasModelPermission | HasProjectPermission, ))
def navigation(self, request, pk=None, section_id=None):
project = self.get_object()

try:
section = project.catalog.sections.get(pk=section_id)
except ObjectDoesNotExist:
raise NotFound()

project.catalog.prefetch_elements()

navigation = compute_navigation(section, project)
return Response(navigation)

@action(detail=True, permission_classes=(HasModelPermission | HasProjectPermission, ))
def resolve(self, request, pk=None):
snapshot_id = request.GET.get('snapshot')
Expand Down

0 comments on commit 464977d

Please sign in to comment.