Skip to content

Commit e83fd06

Browse files
authored
Merge pull request #117 from vnthanhdng/main
feat: Survey system overhaul; per-course surveys, versioning, and statistics
2 parents 994377b + 199f697 commit e83fd06

19 files changed

Lines changed: 1882 additions & 780 deletions
Lines changed: 239 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,143 +1,303 @@
11
import { Request, Response } from "express";
22
import Survey, { ISurvey } from "../models/surveyModel";
3+
import Course from "../models/courseModel";
4+
import SurveyResponse from "../models/surveyResponseModel";
35

4-
// @desc Get the single survey
5-
// @route GET /api/surveys
6-
// @access Public
7-
export const getSurvey = async (req: Request, res: Response): Promise<void> => {
6+
// @desc Get all active surveys
7+
// @route GET /api/surveys?courseId=
8+
export const getSurveys = async(req: Request, res: Response): Promise<void> => {
89
try {
9-
// Use `findOneAndUpdate` to find the survey and create one if it doesn't exist
10-
const survey = await Survey.findOneAndUpdate(
11-
{}, // Find the first document (survey)
12-
{ $setOnInsert: { questions: [] } }, // If no document is found, insert a new one with empty questions
13-
{ new: true, upsert: true } // Return the updated/new document, and if not found, create it
14-
).populate({
10+
const { courseId } = req.query;
11+
const filter: any = {isActive: true};
12+
if (courseId) filter.courseIds = courseId;
13+
14+
const surveys = await Survey.find(filter).populate({
1515
path: "questions",
1616
model: "Question",
1717
});
1818

19-
res.status(200).json(survey);
19+
res.status(200).json({success: true, data: surveys});
2020
} catch (error) {
2121
if (error instanceof Error) {
22-
res.status(500).json({ message: error.message });
22+
res.status(500).json({success: false, message: error.message});
2323
} else {
24-
res.status(500).json({ message: "An unknown error occurred" });
24+
res.status(500).json({success: false, message: "Internal service error."});
2525
}
2626
}
2727
};
2828

29-
// @desc Create a new survey (only if it doesn't already exist)
30-
// @route POST /api/surveys
31-
// @access Public
32-
export const createSurvey = async (
33-
req: Request,
34-
res: Response
35-
): Promise<void> => {
29+
// @desc Get survey by ID
30+
// @route GET /api/surveys/:id
31+
export const getSurveyById = async(req: Request, res: Response): Promise<void> => {
3632
try {
37-
// Check if a survey already exists
38-
const existingSurvey = await Survey.findOne();
39-
40-
if (existingSurvey) {
41-
res.status(200).json({
42-
survey: existingSurvey,
43-
message: "Survey already exists. You can update it instead.",
44-
});
33+
const survey = await Survey.findById(req.params.id).populate({
34+
path: "questions",
35+
model: "Question",
36+
});
37+
38+
if (!survey) {
39+
res.status(404).json({success: false, message: "Survey not found."});
4540
return;
4641
}
42+
43+
res.status(200).json({survey});
44+
} catch (error) {
45+
if (error instanceof Error) {
46+
res.status(500).json({success: false, message: error.message});
47+
} else {
48+
res.status(500).json({success: false, message: "Internal service error."});
49+
}
50+
}
51+
};
4752

48-
const { questions } = req.body;
53+
// @desc Create new survey
54+
// @route POST /api/surveys
55+
export const createSurvey = async(req: Request, res: Response): Promise<void> => {
56+
try {
57+
const { name, questions, courseIds } = req.body;
4958

50-
if (!questions) {
51-
res.status(400).json({ message: "Questions are required" });
59+
if (!name || !questions) {
60+
res.status(400).json({success: false, message: "Name and questions are required."});
5261
return;
5362
}
54-
55-
// Create a new survey
63+
5664
const survey = new Survey({
65+
name,
5766
questions,
67+
courseIds: courseIds || [],
68+
version: 1,
69+
isActive: true,
5870
});
5971
await survey.save();
6072

61-
res.status(201).json({
62-
survey,
63-
message: "Survey created successfully",
64-
});
73+
// update each linked course's surveyId
74+
if (courseIds && courseIds.length > 0) {
75+
await Course.updateMany(
76+
{ _id: { $in: courseIds } },
77+
{ surveyId: survey._id }
78+
);
79+
}
80+
81+
res.status(201).json({success: true, data: survey});
6582
} catch (error) {
6683
if (error instanceof Error) {
67-
res.status(500).json({ message: error.message });
84+
res.status(500).json({success: false, message: error.message});
6885
} else {
69-
res.status(500).json({ message: "An unknown error occurred" });
86+
res.status(500).json({success: false, message: "Internal service error."});
7087
}
7188
}
7289
};
7390

74-
// @desc Update the existing survey
75-
// @route PUT /api/surveys
76-
// @access Public
77-
export const updateSurvey = async (
78-
req: Request,
79-
res: Response
80-
): Promise<void> => {
91+
// @desc Update survey (copy-on-write versioning if responses exist)
92+
// @route PUT /api/surveys/:id
93+
// @body { name?, questions?, courseIdsToUpdate?: string[] }
94+
//
95+
// courseIdsToUpdate controls which courses get the new version.
96+
// Courses not listed keep pointing to the old version.
97+
// If omitted, All linked courses will get the new version.
98+
export const updateSurvey = async(req: Request, res: Response) : Promise<void> => {
8199
try {
82-
const { questions } = req.body;
100+
const {name, questions, courseIdsToUpdate} = req.body;
101+
const survey = await Survey.findById(req.params.id);
83102

84-
console.log(questions);
103+
if (!survey) {
104+
res.status(404).json({success: false, message: "Survey not found."});
105+
return;
106+
}
85107

86-
// Find the single existing survey (since there is only one survey)
87-
const survey = await Survey.findOne();
108+
// Check if there are existing responses for this survey
109+
const responseCount = await SurveyResponse.countDocuments({surveyId: survey._id});
88110

89-
if (!survey) {
90-
res.status(404).json({ message: "Survey not found" });
111+
if (responseCount === 0) {
112+
// No responses, safe to update in place
113+
if (name) survey.name = name;
114+
if (questions) survey.questions = questions;
115+
116+
await survey.save();
117+
118+
const populated = await survey.populate({ path: "questions", model: "Question" });
119+
res.status(200).json({success: true, data: populated});
91120
return;
92121
}
93122

94-
// Update the survey with new questions
95-
survey.questions = questions;
123+
const coursesToUpdate = courseIdsToUpdate || survey.courseIds.map(id => id.toString());
124+
125+
const coursesStaying = survey.courseIds.map(id => id.toString()).filter(id => !coursesToUpdate.includes(id));
126+
127+
const newSurvey = new Survey({
128+
name: name || survey.name,
129+
questions: questions || survey.questions,
130+
courseIds: coursesToUpdate,
131+
version: survey.version + 1,
132+
parentSurveyId: survey._id,
133+
isActive: true,
134+
});
135+
await newSurvey.save();
136+
137+
// Deactive old survey if no courses remain, otherwise trim its courseIds
138+
if (coursesStaying.length === 0) {
139+
survey.isActive = false;
140+
survey.courseIds = [];
141+
} else {
142+
survey.courseIds = coursesStaying as any;
143+
}
96144
await survey.save();
97-
console.log("survey", survey);
98145

99-
res.status(200).json(survey);
146+
// Point updating courses to new survey
147+
await Course.updateMany(
148+
{ _id: { $in: coursesToUpdate } },
149+
{ surveyId: newSurvey._id }
150+
);
151+
152+
const populated = await newSurvey.populate({ path: "questions", model: "Question" });
153+
res.status(200).json({success: true, data: populated});
154+
} catch (error) {
155+
if (error instanceof Error) {
156+
res.status(500).json({success: false, message: error.message});
157+
} else {
158+
res.status(500).json({success: false, message: "Internal service error."});
159+
}
160+
}
161+
};
162+
163+
// @desc Delete survey
164+
// @route DELETE /api/surveys/:id
165+
export const deleteSurvey = async(req: Request, res: Response): Promise<void> => {
166+
try {
167+
const survey = await Survey.findById(req.params.id);
168+
169+
if (!survey) {
170+
res.status(404).json({success: false, message: "Survey not found."});
171+
return;
172+
}
173+
174+
// Unlink from courses
175+
await Course.updateMany({ surveyId: survey._id }, { surveyId: null });
176+
177+
const responseCount = await SurveyResponse.countDocuments({surveyId: survey._id});
178+
if (responseCount > 0) {
179+
// If there are responses, just mark as inactive instead of deleting
180+
survey.isActive = false;
181+
survey.courseIds = [];
182+
await survey.save();
183+
} else {
184+
// No responses, safe to delete
185+
await Survey.deleteOne({ _id: survey._id });
186+
}
187+
188+
res.status(200).json({success: true, message: "Survey deleted successfully."});
100189
} catch (error) {
101190
if (error instanceof Error) {
102-
console.error(error);
103-
res.status(500).json({ message: error.message });
191+
res.status(500).json({success: false, message: error.message});
104192
} else {
105-
res.status(500).json({ message: "An unknown error occurred" });
193+
res.status(500).json({success: false, message: "Internal service error."});
106194
}
107195
}
108196
};
109197

110-
// @desc Delete the survey
111-
// @route DELETE /api/surveys
112-
// @access Public
113-
export const deleteSurvey = async (
114-
req: Request,
115-
res: Response
116-
): Promise<void> => {
198+
// @desc Assign survey to course
199+
// @route POST /api/surveys/:id/assign
200+
// @body {courseId}
201+
export const assignSurvey = async(req: Request, res: Response): Promise<void> => {
117202
try {
118-
// Since there is only one survey, no need to look for it by ID
119-
const survey = await Survey.findOne();
203+
const { courseId } = req.body;
204+
if (!courseId) {
205+
res.status(400).json({success: false, message: "courseId is required."});
206+
return;
207+
}
120208

209+
const survey = await Survey.findById(req.params.id);
121210
if (!survey) {
122-
res.status(404).json({
123-
success: false,
124-
message: "Survey not found",
125-
});
211+
res.status(404).json({success: false, message: "Survey not found."});
126212
return;
127213
}
128214

129-
// Delete the survey
130-
await Survey.deleteOne({ _id: survey._id });
215+
// Add courseId, avoid duplicates
216+
if (!survey.courseIds.includes(courseId)) {
217+
survey.courseIds.push(courseId);
218+
await survey.save();
219+
}
220+
221+
await Course.findByIdAndUpdate(courseId, { surveyId: survey._id });
222+
223+
res.status(200).json({success: true, data: survey, message: "Survey assigned to course successfully."});
224+
} catch (error) {
225+
if (error instanceof Error) {
226+
res.status(500).json({success: false, message: error.message});
227+
} else {
228+
res.status(500).json({success: false, message: "Internal service error."});
229+
}
230+
}
231+
};
131232

132-
res.status(200).json({
133-
success: true,
134-
message: "Survey deleted successfully",
233+
// @desc Unassign survey from a course
234+
// @route POST /api/surveys/:id/unassign
235+
// @body { courseId }
236+
export const unassignSurvey = async (req: Request, res: Response): Promise<void> => {
237+
try {
238+
const { courseId } = req.body;
239+
if (!courseId) {
240+
res.status(400).json({ success: false, message: "courseId is required" });
241+
return;
242+
}
243+
244+
const survey = await Survey.findById(req.params.id);
245+
if (!survey) {
246+
res.status(404).json({ success: false, message: "Survey not found" });
247+
return;
248+
}
249+
250+
survey.courseIds = survey.courseIds.filter(
251+
(id: any) => id.toString() !== courseId
252+
) as any;
253+
await survey.save();
254+
255+
await Course.findByIdAndUpdate(courseId, { surveyId: null });
256+
res.status(200).json({ success: true, data: survey });
257+
} catch (error) {
258+
if (error instanceof Error) {
259+
res.status(500).json({ success: false, message: error.message });
260+
} else {
261+
res.status(500).json({ success: false, message: "An unknown error occurred" });
262+
}
263+
}
264+
};
265+
266+
267+
268+
// @desc Duplicate survey as independent copy
269+
// @route POST /api/surveys/:id/duplicate
270+
// @body { name?, courseId? }
271+
export const duplicateSurvey = async (req: Request, res: Response): Promise<void> => {
272+
try {
273+
const { name, courseId } = req.body;
274+
const original = await Survey.findById(req.params.id);
275+
276+
if (!original) {
277+
res.status(404).json({ success: false, message: "Survey not found" });
278+
return;
279+
}
280+
281+
const duplicate = new Survey({
282+
name: name || `${original.name} (Copy)`,
283+
questions: [...original.questions],
284+
courseIds: courseId ? [courseId] : [],
285+
version: 1,
286+
isActive: true,
135287
});
288+
await duplicate.save();
289+
290+
if (courseId) {
291+
await Course.findByIdAndUpdate(courseId, { surveyId: duplicate._id });
292+
}
293+
294+
res.status(201).json({ success: true, data: duplicate });
136295
} catch (error) {
137296
if (error instanceof Error) {
138-
res.status(500).json({ message: error.message });
297+
res.status(500).json({ success: false, message: error.message });
139298
} else {
140-
res.status(500).json({ message: "An unknown error occurred" });
299+
res.status(500).json({ success: false, message: "An unknown error occurred" });
141300
}
142301
}
143302
};
303+

0 commit comments

Comments
 (0)