Skip to content

Commit d434107

Browse files
feat(surveys): add course user selector tabs to response index page
1 parent 524e52e commit d434107

File tree

22 files changed

+329
-188
lines changed

22 files changed

+329
-188
lines changed

app/controllers/course/survey/responses_controller.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ class Course::Survey::ResponsesController < Course::Survey::Controller
44

55
def index
66
authorize!(:manage, @survey)
7-
@course_students = current_course.course_users.students.order_alphabetically
7+
@course_users = current_course.course_users.order_alphabetically
8+
@my_students = current_course_user.try(:my_students) || []
89
end
910

1011
def create

app/controllers/course/survey/surveys_controller.rb

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ class Course::Survey::SurveysController < Course::Survey::Controller
55
skip_load_and_authorize_resource :survey, only: [:new, :create]
66
build_and_authorize_new_lesson_plan_item :survey, class: Course::Survey, through: :course, only: [:new, :create]
77

8+
COURSE_USERS = { my_students: 'my_students',
9+
my_students_w_phantom: 'my_students_w_phantom',
10+
students: 'students',
11+
students_w_phantom: 'students_w_phantom' }.freeze
12+
813
def index
914
@surveys = @surveys.includes(responses: { experience_points_record: :course_user })
1015
preload_student_submitted_responses_counts
@@ -45,8 +50,14 @@ def results
4550

4651
def remind
4752
authorize!(:manage, @survey)
53+
return head :bad_request unless student_course_users
54+
4855
Course::Survey::ReminderService.
49-
send_closing_reminder(@survey, include_phantom: params[:include_phantom], include_unsubscribed: true)
56+
send_closing_reminder(
57+
@survey,
58+
student_course_users.pluck(:id),
59+
include_unsubscribed: true
60+
)
5061
head :ok
5162
end
5263

@@ -61,6 +72,21 @@ def download
6172

6273
private
6374

75+
def student_course_users
76+
case params[:course_users]
77+
when COURSE_USERS[:my_students]
78+
current_course_user.my_students.without_phantom_users
79+
when COURSE_USERS[:my_students_w_phantom]
80+
current_course_user.my_students
81+
when COURSE_USERS[:students_w_phantom]
82+
@survey.course.course_users.students
83+
when COURSE_USERS[:students]
84+
@survey.course.course_users.students.without_phantom_users
85+
else
86+
false
87+
end
88+
end
89+
6490
def render_survey_with_questions_json
6591
load_sections
6692
render partial: 'survey_with_questions', locals: {

app/services/course/survey/reminder_service.rb

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ def closing_reminder(survey, token)
1515
send_closing_reminder(survey)
1616
end
1717

18-
def send_closing_reminder(survey, include_phantom: true, include_unsubscribed: false)
19-
students = uncompleted_subscribed_students(survey, include_phantom, include_unsubscribed)
18+
def send_closing_reminder(survey, course_user_ids = [], include_unsubscribed: false)
19+
students = uncompleted_subscribed_students(survey, course_user_ids, include_unsubscribed)
2020
unless students.empty?
2121
closing_reminder_students(survey, students)
2222
closing_reminder_staff(survey, students)
@@ -55,19 +55,22 @@ def closing_reminder_staff(survey, students)
5555
# Returns a Set of students who have not completed the given survey and subscribe to the survey email.
5656
#
5757
# @param [Course::Survey] survey The survey to query.
58-
# @param [Boolean] include_phantom Whether to include phantom students in the reminder email.
58+
# @param [Array<Integer>] course_user_ids Course user ids of intended recipients (if specified).
59+
# If empty, all students will be selected.
5960
# @param [Boolean] include_unsubscribed Whether to include unsubscribed students in the reminder (forced reminder).
6061
# @return [Set<CourseUser>] Set of CourseUsers who have not finished the survey.
61-
def uncompleted_subscribed_students(survey, include_phantom, include_unsubscribed)
62-
course_users = survey.course.course_users.student
62+
# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
63+
def uncompleted_subscribed_students(survey, course_user_ids, include_unsubscribed)
64+
course_users = survey.course.course_users
65+
course_users = course_users.where(id: course_user_ids) unless course_user_ids.empty?
6366
email_enabled = survey.course.email_enabled(:surveys, :closing_reminder)
6467
# Eager load :user as it's needed for the recipient email.
65-
students = if (email_enabled.regular && !email_enabled.phantom) || !include_phantom
66-
course_users.without_phantom_users.includes(:user)
68+
students = if email_enabled.regular && !email_enabled.phantom
69+
course_users.student.without_phantom_users.includes(:user)
6770
elsif email_enabled.phantom && !email_enabled.regular
68-
course_users.phantom.includes(:user)
71+
course_users.student.phantom.includes(:user)
6972
else
70-
course_users.includes(:user)
73+
course_users.student.includes(:user)
7174
end
7275

7376
submitted =
@@ -79,4 +82,5 @@ def uncompleted_subscribed_students(survey, include_phantom, include_unsubscribe
7982
where('course_user_email_unsubscriptions.course_settings_email_id = ?', email_enabled.id)
8083
Set.new(students) - Set.new(unsubscribed) - Set.new(submitted)
8184
end
85+
# rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
8286
end

app/views/course/survey/responses/index.json.jbuilder

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
# frozen_string_literal: true
2+
my_students_set = Set.new(@my_students.map(&:id))
23
responses = @responses.to_h { |r| [r.course_user_id, r] }
3-
json.responses @course_students do |student|
4-
response = responses[student.id]
4+
json.responses @course_users do |course_user|
5+
response = responses[course_user.id]
56
can_read_answers = response.present? && can?(:read_answers, response)
67

78
json.course_user do
8-
json.(student, :id, :name)
9-
json.phantom student.phantom?
10-
json.path course_user_path(current_course, student)
9+
json.(course_user, :id, :name)
10+
json.phantom course_user.phantom?
11+
json.path course_user_path(current_course, course_user)
12+
json.isStudent course_user.student?
13+
json.myStudent my_students_set.include?(course_user.id) if course_user.student?
1114
end
1215

1316
json.present !response.nil?

client/app/api/course/Survey/Surveys.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,9 @@ export default class SurveysAPI extends BaseSurveyAPI {
131131
* success response: {}
132132
* error response: {}
133133
*/
134-
remind(includePhantom) {
134+
remind(courseUsers) {
135135
return this.client.post(`${this.#urlPrefix}/${this.getSurveyId()}/remind`, {
136-
include_phantom: includePhantom,
136+
course_users: courseUsers,
137137
});
138138
}
139139

client/app/bundles/course/assessment/submission/constants.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,6 @@ export const TestCaseTypes = {
3939
Evaluation: 'evaluation_test',
4040
};
4141

42-
export const CourseUserTypeDisplayMapper = {
43-
my_students: 'MY STUDENTS',
44-
my_students_w_phantom: 'MY STUDENTS INCL. PHANTOM',
45-
students: 'STUDENTS',
46-
students_w_phantom: 'STUDENTS INCL. PHANTOM',
47-
staff: 'STAFF',
48-
staff_w_phantom: 'STAFF INCL. PHANTOM',
49-
};
50-
5142
export const scribingPopoverTypes = mirrorCreator([
5243
'TYPE',
5344
'DRAW',

client/app/bundles/course/assessment/submission/pages/SubmissionsIndex/index.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { MainSubmissionInfo } from 'types/course/statistics/assessmentStatistics';
1111

1212
import SubmissionStatusChart from 'course/assessment/pages/AssessmentStatistics/SubmissionStatus/SubmissionStatusChart';
13+
import CourseUserTypeFragment from 'lib/components/core/CourseUserTypeFragment';
1314
import CourseUserTypeTabs, {
1415
CourseUserType,
1516
CourseUserTypeTabValue,
@@ -38,7 +39,7 @@ import {
3839
sendAssessmentReminderEmail,
3940
unsubmitAllSubmissions,
4041
} from '../../actions/submissions';
41-
import { CourseUserTypeDisplayMapper, workflowStates } from '../../constants';
42+
import { workflowStates } from '../../constants';
4243
import translations from '../../translations';
4344

4445
import SubmissionsTable from './SubmissionsTable';
@@ -204,7 +205,9 @@ const AssessmentSubmissionsIndex: FC = () => {
204205
attempting: shownSubmissions.filter(
205206
(s) => s.workflowState === workflowStates.Attempting,
206207
).length,
207-
selectedUsers: CourseUserTypeDisplayMapper[currentSelectedUserType],
208+
selectedUsers: (
209+
<CourseUserTypeFragment userType={currentSelectedUserType} />
210+
),
208211
};
209212
const message = assessment.autograded
210213
? translations.forceSubmitConfirmationAutograded
@@ -360,7 +363,9 @@ const AssessmentSubmissionsIndex: FC = () => {
360363
graded: shownSubmissions.filter(
361364
(s) => s.workflowState === workflowStates.Graded,
362365
).length,
363-
selectedUsers: CourseUserTypeDisplayMapper[currentSelectedUserType],
366+
selectedUsers: (
367+
<CourseUserTypeFragment userType={currentSelectedUserType} />
368+
),
364369
};
365370

366371
return (
@@ -384,7 +389,9 @@ const AssessmentSubmissionsIndex: FC = () => {
384389
attempting: shownSubmissions.filter(
385390
(s) => s.workflowState === workflowStates.Attempting,
386391
).length,
387-
selectedUsers: CourseUserTypeDisplayMapper[currentSelectedUserType],
392+
selectedUsers: (
393+
<CourseUserTypeFragment userType={currentSelectedUserType} />
394+
),
388395
};
389396

390397
return (

client/app/bundles/course/survey/actions/surveys.js

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,15 +169,11 @@ export function fetchResults(surveyId) {
169169
};
170170
}
171171

172-
export function sendReminderEmail(
173-
successMessage,
174-
failureMessage,
175-
includePhantom,
176-
) {
172+
export function sendReminderEmail(successMessage, failureMessage, courseUsers) {
177173
return (dispatch) => {
178174
dispatch({ type: actionTypes.SEND_REMINDER_REQUEST });
179175
return CourseAPI.survey.surveys
180-
.remind(includePhantom)
176+
.remind(courseUsers)
181177
.then(() => {
182178
dispatch({ type: actionTypes.SEND_REMINDER_SUCCESS });
183179
setNotification(successMessage)(dispatch);

client/app/bundles/course/survey/pages/ResponseIndex/RemindButton.jsx

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Button } from '@mui/material';
55
import PropTypes from 'prop-types';
66

77
import { sendReminderEmail } from 'course/survey/actions/surveys';
8+
import CourseUserTypeFragment from 'lib/components/core/CourseUserTypeFragment';
89
import ConfirmationDialog from 'lib/components/core/dialogs/ConfirmationDialog';
910

1011
const translations = defineMessages({
@@ -21,12 +22,7 @@ const translations = defineMessages({
2122
confirmation: {
2223
id: 'course.survey.ResponseIndex.RemindButton.confirmation',
2324
defaultMessage:
24-
'Send emails to all students (excluding phantoms) who have not completed the survey?',
25-
},
26-
confirmationAll: {
27-
id: 'course.survey.ResponseIndex.RemindButton.confirmationAll',
28-
defaultMessage:
29-
'Send emails to all students (including phantoms) who have not completed the survey?',
25+
'Send reminder emails to all {selectedUsers} who have not completed the survey?',
3026
},
3127
success: {
3228
id: 'course.survey.ResponseIndex.RemindButton.success',
@@ -48,11 +44,7 @@ class RemindButton extends Component {
4844
const successMessage = <FormattedMessage {...translations.success} />;
4945
const failureMessage = <FormattedMessage {...translations.failure} />;
5046
this.props.dispatch(
51-
sendReminderEmail(
52-
successMessage,
53-
failureMessage,
54-
this.props.includePhantom,
55-
),
47+
sendReminderEmail(successMessage, failureMessage, this.props.userType),
5648
);
5749
this.setState({ open: false });
5850
};
@@ -72,11 +64,14 @@ class RemindButton extends Component {
7264
<FormattedMessage {...translations.explanation} />
7365
<br />
7466
<br />
75-
{this.props.includePhantom ? (
76-
<FormattedMessage {...translations.confirmationAll} />
77-
) : (
78-
<FormattedMessage {...translations.confirmation} />
79-
)}
67+
<FormattedMessage
68+
{...translations.confirmation}
69+
values={{
70+
selectedUsers: (
71+
<CourseUserTypeFragment userType={this.props.userType} />
72+
),
73+
}}
74+
/>
8075
</>
8176
}
8277
onCancel={() => this.setState({ open: false })}
@@ -90,7 +85,7 @@ class RemindButton extends Component {
9085

9186
RemindButton.propTypes = {
9287
dispatch: PropTypes.func.isRequired,
93-
includePhantom: PropTypes.bool.isRequired,
88+
userType: PropTypes.string.isRequired,
9489
};
9590

9691
export default connect()(RemindButton);

client/app/bundles/course/survey/pages/ResponseIndex/__test__/RemindButton.test.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { fireEvent, render } from 'test-utils';
22

33
import CourseAPI from 'api/course';
4+
import { CourseUserType } from 'lib/components/core/CourseUserTypeTabs';
45

56
import RemindButton from '../RemindButton';
67

78
describe('<RemindButton />', () => {
89
it('renders confirmation dialog that triggers the reminder', () => {
910
const spyRemind = jest.spyOn(CourseAPI.survey.surveys, 'remind');
10-
const page = render(<RemindButton includePhantom />);
11+
const page = render(<RemindButton userType={CourseUserType.STUDENTS} />);
1112

1213
const button = page.getByRole('button');
1314
fireEvent.click(button);

0 commit comments

Comments
 (0)