Skip to content

Commit 404cebd

Browse files
feat(courses): implement course suspension flag
- renamed "suspension_message" to "user_suspension_message" - reworded / added relevant course settings form elements
1 parent cf71686 commit 404cebd

22 files changed

Lines changed: 370 additions & 59 deletions

File tree

app/controllers/course/admin/admin_controller.rb

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,28 @@ def destroy
3030
end
3131
end
3232

33+
def suspend
34+
authorize!(:manage, current_course)
35+
current_course.update!(is_suspended: true)
36+
true
37+
end
38+
39+
def unsuspend
40+
authorize!(:manage, current_course)
41+
current_course.update!(is_suspended: false)
42+
true
43+
end
44+
3345
private
3446

3547
def course_setting_params
3648
params.require(:course).
37-
permit(:title, :description, :published, :enrollable, :enrol_auto_approve, :start_at, :end_at, :logo, :gamified,
38-
:show_personalized_timeline_features, :default_timeline_algorithm, :suspension_message,
49+
permit(:title, :description,
50+
:published, :enrollable, :enrol_auto_approve,
51+
:start_at, :end_at,
52+
:logo, :gamified,
53+
:show_personalized_timeline_features, :default_timeline_algorithm,
54+
:user_suspension_message, :course_suspension_message,
3955
:time_zone, :advance_start_at_duration_days)
4056
end
4157

app/mailers/course/mailer.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def user_suspended_email(course_user)
134134
@course = course_user.course
135135
end
136136
@recipient = course_user.user
137-
@suspension_message = @course.suspension_message.presence
137+
@user_suspension_message = @course.user_suspension_message.presence
138138

139139
I18n.with_locale(@recipient.locale) do
140140
mail(to: @recipient.email, subject: t('.subject', course: @course.title))

app/views/course/admin/admin/index.json.jbuilder

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,7 @@ json.defaultTimelineAlgorithm current_course.default_timeline_algorithm
1313
json.timeZone current_course.time_zone
1414
json.advanceStartAtDurationDays current_course.advance_start_at_duration_days
1515
json.canDelete can?(:destroy, current_course)
16-
json.suspensionMessage current_course.suspension_message unless current_course.suspension_message.blank?
16+
json.canSuspendCourse can?(:manage, current_course)
17+
json.userSuspensionMessage current_course.user_suspension_message unless current_course.user_suspension_message.blank?
18+
json.isSuspended current_course.is_suspended
19+
json.courseSuspensionMessage current_course.course_suspension_message unless current_course.course_suspension_message.blank?

app/views/course/courses/_course_data.json.jbuilder

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,4 @@ end
5757

5858
is_suspended_user = current_course_user&.is_suspended && cannot?(:manage, current_course)
5959
json.isSuspendedUser is_suspended_user
60-
json.suspensionMessage current_course.suspension_message if is_suspended_user
60+
json.userSuspensionMessage current_course.user_suspension_message if is_suspended_user
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
= simple_format(t('.message', course: link_to(@course.title, course_url(@course, host: @course.instance.host)),
22
coursemology: link_to(t('common.mailers.coursemology'), @course.instance.host)))
3-
= simple_format(@suspension_message || t('.default_suspension_message'))
3+
= simple_format(@user_suspension_message || t('.default_suspension_message'))
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
<%= t('.message', course: plain_link_to(@course.title, course_url(@course, host: @course.instance.host)),
22
coursemology: plain_link_to(t('common.mailers.coursemology'), @course.instance.host)) %>
33

4-
<%= @suspension_message || t('.default_suspension_message') %>
4+
<%= @user_suspension_message || t('.default_suspension_message') %>

client/app/api/course/Admin/Course.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,12 @@ export default class CourseAdminAPI extends BaseAdminAPI {
3434
delete(): Promise<AxiosResponse> {
3535
return this.client.delete(this.urlPrefix);
3636
}
37+
38+
suspend(): Promise<AxiosResponse> {
39+
return this.client.patch(`${this.urlPrefix}/suspend`);
40+
}
41+
42+
unsuspend(): Promise<AxiosResponse> {
43+
return this.client.patch(`${this.urlPrefix}/unsuspend`);
44+
}
3745
}

client/app/bundles/common/ErrorPage.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,14 @@ const translations = defineMessages({
6161
'Graphic of an astronaut floating in space is created by <author>Storyset</author> from ' +
6262
'<source>www.storyset.com</source>, with modifications.',
6363
},
64-
suspended: {
65-
id: 'app.ErrorPage.suspended',
64+
userSuspended: {
65+
id: 'app.ErrorPage.userSuspended',
6666
defaultMessage: 'Your access to this course has been suspended.',
6767
},
68+
courseSuspended: {
69+
id: 'app.ErrorPage.courseSuspended',
70+
defaultMessage: 'This course has been suspended.',
71+
},
6872
error: {
6973
id: 'app.ErrorPage.error',
7074
defaultMessage: 'KABOOM, a meteor has just crashed.',
@@ -285,10 +289,10 @@ const SuspendedPage = (): JSX.Element => {
285289
illustrationAlt="Forbidden illustration"
286290
illustrationSrc={forbiddenIllustration}
287291
subtitle={
288-
course?.suspensionMessage ?? t(courseTranslations.suspendedSubtitle)
292+
course?.userSuspensionMessage ?? t(courseTranslations.suspendedSubtitle)
289293
}
290294
tip={sourceURL}
291-
title={t(translations.suspended)}
295+
title={t(translations.userSuspended)}
292296
/>
293297
);
294298
};

client/app/bundles/course/admin/pages/CourseSettings/CourseSettingsForm.tsx

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { CourseInfo, TimeOffset, TimeZones } from 'types/course/admin/course';
66
import { getCourseLogoUrl } from 'course/helper';
77
import AvatarSelector from 'lib/components/core/AvatarSelector';
88
import RadioButton from 'lib/components/core/buttons/RadioButton';
9+
import Prompt, { PromptText } from 'lib/components/core/dialogs/Prompt';
910
import InfoLabel from 'lib/components/core/InfoLabel';
1011
import Section from 'lib/components/core/layouts/Section';
1112
import Subsection from 'lib/components/core/layouts/Subsection';
@@ -28,6 +29,8 @@ interface CourseSettingsFormProps {
2829
timeZones: TimeZones;
2930
onSubmit: (data: CourseInfo, timeOffset?: TimeOffset) => void;
3031
onDeleteCourse: () => void;
32+
onSuspendCourse: () => void;
33+
onUnsuspendCourse: () => void;
3134
onUploadCourseLogo: (image: File, onSuccess: () => void) => void;
3235
disabled: boolean;
3336
}
@@ -38,11 +41,13 @@ const CourseSettingsForm = forwardRef<
3841
>((props, ref): JSX.Element => {
3942
const { t } = useTranslation();
4043
const [offsetTimesPrompt, setOffsetTimesPrompt] = useState(false);
44+
const [suspendingCourse, setSuspendingCourse] = useState(false);
4145
const [deletingCourse, setDeletingCourse] = useState(false);
4246
const [stagedLogo, setStagedLogo] = useState<File>();
4347

4448
const closeOffsetTimesPrompt = (): void => setOffsetTimesPrompt(false);
4549
const closeDeleteCoursePrompt = (): void => setDeletingCourse(false);
50+
const closeSuspendingCoursePrompt = (): void => setSuspendingCourse(false);
4651

4752
const timeZonesOptions = useMemo(
4853
() =>
@@ -327,13 +332,72 @@ const CourseSettingsForm = forwardRef<
327332
</Section>
328333

329334
<Section sticksToNavbar title={t(translations.suspension)}>
335+
{props.data.canSuspendCourse && (
336+
<>
337+
<Typography variant="body2">
338+
{t(translations.suspendCourseDescription)}
339+
</Typography>
340+
{!props.data.isSuspended && (
341+
<Button
342+
color="warning"
343+
disabled={props.disabled}
344+
onClick={() => setSuspendingCourse(true)}
345+
variant="outlined"
346+
>
347+
{t(translations.suspendCourse)}
348+
</Button>
349+
)}
350+
<Prompt
351+
onClickPrimary={() => {
352+
props.onSuspendCourse();
353+
setSuspendingCourse(false);
354+
}}
355+
onClose={closeSuspendingCoursePrompt}
356+
open={suspendingCourse}
357+
primaryColor="warning"
358+
primaryLabel={t(translations.suspendCourse)}
359+
>
360+
<PromptText>
361+
{t(translations.suspendCoursePromptText)}
362+
</PromptText>
363+
</Prompt>
364+
{props.data.isSuspended && (
365+
<Button
366+
color="warning"
367+
disabled={props.disabled}
368+
onClick={props.onUnsuspendCourse}
369+
variant="outlined"
370+
>
371+
{t(translations.unsuspendCourse)}
372+
</Button>
373+
)}
374+
<Subsection
375+
subtitle={t(translations.courseSuspensionMessageDescription)}
376+
title={t(translations.courseSuspensionMessage)}
377+
>
378+
<Controller
379+
control={control}
380+
name="courseSuspensionMessage"
381+
render={({ field, fieldState }): JSX.Element => (
382+
<FormTextField
383+
disabled={props.disabled}
384+
field={field}
385+
fieldState={fieldState}
386+
fullWidth
387+
placeholder={t(courseTranslations.suspendedSubtitle)}
388+
/>
389+
)}
390+
/>
391+
</Subsection>
392+
</>
393+
)}
330394
<Subsection
331-
subtitle={t(translations.suspensionMessageDescription)}
332-
title={t(translations.suspensionMessage)}
395+
subtitle={t(translations.userSuspensionMessageDescription)}
396+
title={t(translations.userSuspensionMessage)}
333397
>
334398
<Controller
335399
control={control}
336-
name="suspensionMessage"
400+
name="userSuspensionMessage"
337401
render={({ field, fieldState }): JSX.Element => (
338402
<FormTextField
339403
disabled={props.disabled}

client/app/bundles/course/admin/pages/CourseSettings/index.tsx

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ComponentRef, useRef, useState } from 'react';
2+
import { useNavigate } from 'react-router-dom';
23
import { CourseInfo, TimeOffset, TimeZones } from 'types/course/admin/course';
34

45
import LoadingIndicator from 'lib/components/core/LoadingIndicator';
@@ -14,6 +15,8 @@ import {
1415
deleteCourse,
1516
fetchCourseSettings,
1617
fetchTimeZones,
18+
suspendCourse,
19+
unsuspendCourse,
1720
updateCourseLogo,
1821
updateCourseSettings,
1922
} from './operations';
@@ -29,6 +32,8 @@ const CourseSettings = (): JSX.Element => {
2932
const [reloadForm, setReloadForm] = useState(false);
3033
const [submitting, setSubmitting] = useState(false);
3134

35+
const navigate = useNavigate();
36+
3237
const updateForm = (data?: CourseInfo): void => {
3338
if (!data) return;
3439
formRef.current?.resetTo?.(data);
@@ -76,15 +81,44 @@ const CourseSettings = (): JSX.Element => {
7681
deleteCourse()
7782
.then(() => {
7883
toast.success(t(translations.deleteCourseSuccess));
79-
// TODO: Replace this with useNavigate()('/courses') once SPA
80-
window.location.replace('/courses');
84+
navigate('/courses');
8185
})
8286
.catch(() => {
8387
toast.error(t(translations.errorOccurredWhenDeletingCourse));
8488
})
8589
.finally(() => setSubmitting(false));
8690
};
8791

92+
const handleSuspendCourse = (): void => {
93+
setSubmitting(true);
94+
95+
suspendCourse()
96+
.then(() => {
97+
formRef.current?.resetByMerging?.({ isSuspended: true });
98+
toast.success(t(translations.suspendCourseSuccess));
99+
setReloadForm((value) => !value);
100+
})
101+
.catch(() => {
102+
toast.error(t(translations.suspendCourseFailure));
103+
})
104+
.finally(() => setSubmitting(false));
105+
};
106+
107+
const handleUnsuspendCourse = (): void => {
108+
setSubmitting(true);
109+
110+
unsuspendCourse()
111+
.then(() => {
112+
formRef.current?.resetByMerging?.({ isSuspended: false });
113+
toast.success(t(translations.unsuspendCourseSuccess));
114+
setReloadForm((value) => !value);
115+
})
116+
.catch(() => {
117+
toast.error(t(translations.unsuspendCourseFailure));
118+
})
119+
.finally(() => setSubmitting(false));
120+
};
121+
88122
return (
89123
<Preload
90124
render={<LoadingIndicator />}
@@ -98,6 +132,8 @@ const CourseSettings = (): JSX.Element => {
98132
disabled={submitting}
99133
onDeleteCourse={handleDeleteCourse}
100134
onSubmit={handleSubmit}
135+
onSuspendCourse={handleSuspendCourse}
136+
onUnsuspendCourse={handleUnsuspendCourse}
101137
onUploadCourseLogo={handleUploadCourseLogo}
102138
timeZones={timeZones}
103139
/>

0 commit comments

Comments
 (0)