Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions couchdb-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ upsert_doc _users _design/_auth @./design/users/_auth.json
upsert_doc resource_activities _design/resource_activities @./design/activities/activities-design.json
upsert_doc login_activities _design/login_activities @./design/activities/activities-design.json
upsert_doc courses_progress _design/courses_progress @./design/courses_progress/courses_progress-design.json
upsert_doc submissions _design/surveyData @./design/submissions/submissions-design.json

# Insert indexes
# Note indexes will not overwrite if fields value changes, so make sure to remove unused indexes after changing
Expand All @@ -183,6 +184,7 @@ upsert_doc resources _index '{"index":{"fields":[{"title":"asc"}]},"name":"time-
upsert_doc news _index '{"index":{"fields":[{"time":"desc"}]},"name":"time-index"}' POST
upsert_doc tags _index '{"index":{"fields":[{"name":"asc"}]},"name":"name-index"}' POST
upsert_doc team_activities _index '{"index":{"fields":[{"time":"desc"}]},"name":"time-index"}' POST
upsert_doc submissions _index '{"index":{"fields":["type","parent"]},"name":"parent-submissions-index"}' POST
# Only insert dummy data and update security on install
# _users security is set in app and auto accept will be overwritten if set here
if (($ISINSTALL))
Expand Down
33 changes: 33 additions & 0 deletions design/submissions/submissions-design.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module.exports = {
"views": {
"surveyData": {
"map": function(doc) {
if (doc.type === 'survey') {
var teamId = doc.team && doc.team._id ? doc.team._id : null;
var status = doc.status || 'pending';

// Emit for counting
emit([doc.parentId, teamId, status], 1);

// Emit parent metadata
if (doc.parent && doc.parent._id) {
emit(['parent', doc.parent._id], {
parentDoc: {
_id: doc.parent._id,
_rev: doc.parent._rev,
name: doc.parent.name,
description: doc.parent.description,
questions: doc.parent.questions,
type: doc.parent.type,
sourcePlanet: doc.parent.sourcePlanet
},
status: status,
teamId: teamId
});
}
}
},
"reduce": "_count"
}
}
};
1 change: 1 addition & 0 deletions design/submissions/submissions-design.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"views":{"surveyData":{"map":"function(doc) {\n if (doc.type === 'survey') {\n var teamId = doc.team && doc.team._id ? doc.team._id : null;\n var status = doc.status || 'pending';\n\n // Emit for counting\n emit([doc.parentId, teamId, status], 1);\n\n // Emit parent metadata\n if (doc.parent && doc.parent._id) {\n emit(['parent', doc.parent._id], {\n parentDoc: {\n _id: doc.parent._id,\n _rev: doc.parent._rev,\n name: doc.parent.name,\n description: doc.parent.description,\n questions: doc.parent.questions,\n type: doc.parent.type,\n sourcePlanet: doc.parent.sourcePlanet\n },\n status: status,\n teamId: teamId\n });\n }\n }\n }","reduce":"_count"}}}
131 changes: 96 additions & 35 deletions src/app/surveys/surveys.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { MatSort } from '@angular/material/sort';
import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table';
import { SelectionModel } from '@angular/cdk/collections';
import { forkJoin, Observable, Subject, throwError, of } from 'rxjs';
import { catchError, switchMap, tap } from 'rxjs/operators';
import { catchError, switchMap, tap, takeUntil } from 'rxjs/operators';
import { CouchService } from '../shared/couchdb.service';
import { ChatService } from '../shared/chat.service';
import { filterSpecificFields, sortNumberOrString, createDeleteArray, selectedOutOfFilter } from '../shared/table-helpers';
Expand Down Expand Up @@ -82,12 +82,13 @@ export class SurveysComponent implements OnInit, AfterViewInit, OnDestroy {
this.surveys.filterPredicate = filterSpecificFields([ 'name' ]);
this.surveys.sortingDataAccessor = sortNumberOrString;
this.loadSurveys();
this.couchService.checkAuthorization(this.dbName).subscribe((isAuthorized) => this.isAuthorized = isAuthorized);
this.couchService.checkAuthorization(this.dbName)
.pipe(takeUntil(this.onDestroy$)).subscribe((isAuthorized) => this.isAuthorized = isAuthorized);
this.surveys.connect().subscribe(surveys => {
this.parentCount = surveys.filter(survey => survey.parent === true).length;
this.surveyCount.emit(surveys.length);
});
this.chatService.listAIProviders().subscribe((providers) => {
this.chatService.listAIProviders().pipe(takeUntil(this.onDestroy$)).subscribe((providers) => {
this.availableAIProviders = providers;
});
}
Expand All @@ -106,45 +107,98 @@ export class SurveysComponent implements OnInit, AfterViewInit, OnDestroy {
this.onDestroy$.complete();
}

private countSubmissionsForSurvey(surveyId: string, countMap: Map<any, any>, targetTeamId: string | null): number {
if (!countMap.has(surveyId)) {
return 0;
}

const surveyTeamCounts = countMap.get(surveyId);
if (targetTeamId) {
return surveyTeamCounts.has(targetTeamId) ? (surveyTeamCounts.get(targetTeamId)['complete'] || 0) : 0;
}

let count = 0;
surveyTeamCounts.forEach(statusCounts => {
count += statusCounts['complete'] || 0;
});
return count;
}

private loadSurveys() {
this.isLoading = true;
const receiveData = (dbName: string, type: string) => this.couchService.findAll(dbName, findDocuments({ 'type': type }));
forkJoin([
receiveData('exams', 'surveys'),
receiveData('submissions', 'survey'),
this.couchService.findAll('courses')
]).subscribe(([ allSurveys, submissions, courses ]: any) => {
this.couchService.get('submissions/_design/surveyData/_view/submissionCounts?group=true'),
this.couchService.findAll('courses'),
this.couchService.get('submissions/_design/surveyData/_view/parentSurveys')
Comment on lines 130 to +134

Choose a reason for hiding this comment

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

P2 Badge Catch errors when fetching submission view data

The new forkJoin now depends on two raw couchService.get view calls. get rethrows on 403/404, so if the new surveyData design doc is missing or the user lacks view access, the request errors and forkJoin never reaches the code that stops the loading spinner or populates the table. Previously this path used findAll('submissions', 'survey'), which returned an empty array on errors, so the UI degraded gracefully. Please add error handling/fallback for these view requests to avoid leaving the component stuck when the view is unavailable.

Useful? React with 👍 / 👎.

]).subscribe(([ allSurveys, submissionCounts, courses, parentSurveyRows ]: any) => {
const countMap = new Map();
submissionCounts.rows.forEach(row => {
const [ parentId, teamId, status ] = row.key;
let parentMap = countMap.get(parentId);
if (!parentMap) {
parentMap = new Map();
countMap.set(parentId, parentMap);
}

let teamCounts = parentMap.get(teamId);
if (!teamCounts) {
teamCounts = {};
parentMap.set(teamId, teamCounts);
}
teamCounts[status] = row.value;
});

const teamSurveys = allSurveys.filter((survey: any) => survey.sourceSurveyId);
const findSurveyInSteps = (steps, survey) => steps.findIndex((step: any) => step.survey && step.survey._id === survey._id);
const teamSurveysMap = new Map<string, any[]>();
teamSurveys.forEach(ts => {
const sourceId = ts.sourceSurveyId;
if (!teamSurveysMap.has(sourceId)) {
teamSurveysMap.set(sourceId, []);
}
teamSurveysMap.get(sourceId).push(ts);
});

const surveyCourseMap = new Map<string, any>();
courses.forEach((course: any) => {
if (course.steps && Array.isArray(course.steps)) {
course.steps.forEach((step: any) => {
if (step.survey && step.survey._id && !surveyCourseMap.has(step.survey._id)) {
surveyCourseMap.set(step.survey._id, course);
}
});
}
});

this.allSurveys = [
...allSurveys.map((survey: any) => {
const directSubmissions = submissions.filter(sub => sub.parentId === survey._id || sub.parentId?.startsWith(survey._id + '@'));
const derivedTeamSurveys = teamSurveys.filter(ts => ts.sourceSurveyId === survey._id);
const derivedSubmissions = derivedTeamSurveys.flatMap(ts =>
submissions.filter(sub => sub.parentId === ts._id || sub.parentId?.startsWith(ts._id + '@'))
);
const relatedSubmissions = [...directSubmissions, ...derivedSubmissions];
const derivedTeamSurveys = teamSurveysMap.get(survey._id) || [];
const teamIds = [
...new Set([
survey.teamId,
...derivedTeamSurveys.map(ts => ts.teamId)
])
].filter(Boolean);

const targetTeamId = this.teamId || this.routeTeamId;
let taken = this.countSubmissionsForSurvey(survey._id, countMap, targetTeamId);
derivedTeamSurveys.forEach(ts => {
taken += this.countSubmissionsForSurvey(ts._id, countMap, targetTeamId);
});

const course = surveyCourseMap.get(survey._id);
return {
...survey,
teamIds: teamIds,
course: courses.find((course: any) => findSurveyInSteps(course.steps, survey) > -1),
taken: this.teamId || this.routeTeamId
? relatedSubmissions.filter(
(data) => data.status === 'complete' &&
(data.team?._id === this.teamId || data.team?._id === this.routeTeamId)).length
: relatedSubmissions.filter(data => data.status === 'complete').length
teamIds,
course,
courseTitle: course ? course.courseTitle : '',
taken
};
}),
...this.createParentSurveys(submissions)
...this.createParentSurveys(parentSurveyRows.rows)
];
this.applyViewModeFilter();
this.surveys.data = this.surveys.data.map((data: any) => ({ ...data, courseTitle: data.course ? data.course.courseTitle : '' }));
this.dialogsLoadingService.stop();
this.isLoading = false;
});
Expand All @@ -165,21 +219,28 @@ export class SurveysComponent implements OnInit, AfterViewInit, OnDestroy {
});
}

createParentSurveys(submissions) {
return submissions.filter(submission => submission.parent).reduce((parentSurveys, submission) => {
const parentSurvey = parentSurveys.find(nSurvey => nSurvey._id === submission.parent._id);
if (parentSurvey) {
parentSurvey.taken = parentSurvey.taken + (submission.status !== 'pending' ? 1 : 0);
} else if (submission.parent.sourcePlanet === this.stateService.configuration.parentCode) {
return [ ...parentSurveys, {
...submission.parent,
taken: submission.status !== 'pending' ? 1 : 0,
createParentSurveys(viewRows) {
// viewRows format: { key: parentId, value: { parentDoc, status, teamId } }
const parentSurveysMap = new Map();

viewRows.forEach(row => {
const { parentDoc, status, teamId } = row.value;
const parentId = parentDoc._id;

if (parentSurveysMap.has(parentId)) {
const parentSurvey = parentSurveysMap.get(parentId);
parentSurvey.taken += (status !== 'pending' ? 1 : 0);
} else if (parentDoc.sourcePlanet === this.stateService.configuration.parentCode) {
parentSurveysMap.set(parentId, {
...parentDoc,
taken: status !== 'pending' ? 1 : 0,
parent: true,
teamId: submission.team?._id
} ];
teamId
});
}
return parentSurveys;
}, []);
});

return Array.from(parentSurveysMap.values());
}

goBack() {
Expand Down
Loading