Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

N21-1774 create synced course #3426

Merged
merged 23 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8254db7
add ifNot helper
MBergCap Mar 14, 2024
06fb5ee
add disabled and hidden fields
MBergCap Mar 14, 2024
1c5626f
hide button
MBergCap Mar 14, 2024
eb07b01
add group sync
MBergCap Mar 14, 2024
17bc9a5
add group sync
MBergCap Mar 14, 2024
a7ccb27
remove duplicated code
MBergCap Mar 15, 2024
389be73
Merge branch 'main' into N21-1774-create-synced-course
MBergCap Mar 15, 2024
bea8f24
change syncedWithGroup
MBergCap Mar 15, 2024
2833c40
change syncedWithGroup
MBergCap Mar 15, 2024
c334827
Merge remote-tracking branch 'origin/N21-1774-create-synced-course' i…
MBergCap Mar 15, 2024
92e1416
add feature flag check
IgorCapCoder Mar 18, 2024
60d0c4a
fix query
MarvinOehlerkingCap Mar 18, 2024
4e520e9
Merge branch 'main' into N21-1774-create-synced-course
IgorCapCoder Mar 19, 2024
3109be1
change variable name
IgorCapCoder Mar 19, 2024
1c32b32
Merge remote-tracking branch 'origin/N21-1774-create-synced-course' i…
IgorCapCoder Mar 19, 2024
eee740d
Merge branch 'main' into N21-1774-create-synced-course
IgorCapCoder Mar 20, 2024
76eef7e
Merge branch 'main' into N21-1774-create-synced-course
arnegns Mar 25, 2024
f1fb546
Revert "change variable name"
MarvinOehlerkingCap Mar 25, 2024
12ec446
fix variable name
MarvinOehlerkingCap Mar 25, 2024
1115dbf
fix query and env
MarvinOehlerkingCap Mar 27, 2024
18fac68
fix unused endpoint and #ifNot
MarvinOehlerkingCap Mar 27, 2024
2b13900
Merge branch 'main' into N21-1774-create-synced-course
IgorCapCoder Apr 8, 2024
79373d3
Merge branch 'main' into N21-1774-create-synced-course
IgorCapCoder Apr 8, 2024
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
5 changes: 5 additions & 0 deletions config/default.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,11 @@
"type": "boolean",
"default": true,
"description": "Uses the v3 api over the v1 api for systems"
},
"FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED": {
"type": "boolean",
"default": false,
"description": "Enables the synchronization of courses with linked groups during provisioning."
}
},
"allOf": [
Expand Down
65 changes: 41 additions & 24 deletions controllers/courses.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,31 @@ const filterStudents = (ctx, s) => (
? s.filter(({ selected }) => selected) : s
);

const getUserIdsByRole = (users, role) => users.filter((u) => u.role === role).map((u) => u.id);

const selectedElementIdsToString = (arr = []) => {
const txt = arr.filter((obj) => obj.selected).map((obj) => (obj.id !== undefined ? obj.id : obj._id)).join(',');
return txt;
};

const getSyncedElementIds = (
const strToPropsArray = (props, keys) => {
keys.forEach((key) => {
if (typeof props[key] === 'string') {
props[key] = props[key].trim() ? props[key].split(',') : [];
}
});
return props;
};

const getSyncedElements = (
course,
classesAndGroups,
classAndGroupIdsOfCourse,
teachers,
substitutions,
students,
res,
syncedWithGroup,
) => {
const startDate = course.startDate ? timesHelper.formatDate(course.startDate, 'DD.MM.YYYY') : undefined;
const untilDate = course.untilDate ? timesHelper.formatDate(course.untilDate, 'DD.MM.YYYY') : undefined;
Expand All @@ -63,6 +75,7 @@ const getSyncedElementIds = (
studentsSelected: selectedElementIdsToString(filterStudents(res, markSelected(students, course.userIds))),
startDate,
untilDate,
syncedWithGroup,
};

return selectedElements;
Expand Down Expand Up @@ -194,13 +207,21 @@ const editCourseHandler = (req, res, next) => {
.get(`/courses/${req.params.courseId}/userPermissions/${res.locals.currentUser._id}`);
}

let syncedGroupId;
let groupPromise;
if (req.query.syncedGroupId && Configuration.get('FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED')) {
syncedGroupId = req.query.syncedGroupId;
groupPromise = api(req, { version: 'v3' }).get(`/groups/${syncedGroupId}`);
}

Promise.all([
coursePromise,
classesAndGroupsPromise,
teachersPromise,
studentsPromise,
scopePermissions,
]).then(([course, _classesAndGroups, _teachers, _students, _scopePermissions]) => {
groupPromise,
]).then(([course, _classesAndGroups, _teachers, _students, _scopePermissions, group]) => {
// these 3 might not change anything because hooks allow just ownSchool results by now, but to be sure:
let classesAndGroups = [];
if (FEATURE_GROUPS_IN_COURSE_ENABLED) {
Expand Down Expand Up @@ -278,14 +299,21 @@ const editCourseHandler = (req, res, next) => {

const classAndGroupIdsOfCourse = [...(course.classIds || []), ...(course.groupIds || [])];

const syncedElementIds = course.syncedWithGroup ? getSyncedElementIds(
if (syncedGroupId && group) {
course.name = group.name;
course.teacherIds = getUserIdsByRole(group.users, 'teacher');
course.userIds = getUserIdsByRole(group.users, 'student');
}

const syncedElements = (course.syncedWithGroup || syncedGroupId) ? getSyncedElements(
course,
classesAndGroups,
classAndGroupIdsOfCourse,
teachers,
substitutions,
students,
res,
syncedGroupId,
) : {};

if (req.params.courseId) {
Expand All @@ -304,7 +332,7 @@ const editCourseHandler = (req, res, next) => {
students: filterStudents(res, markSelected(students, course.userIds)),
scopePermissions: _scopePermissions,
schoolData: res.locals.currentSchoolData,
...syncedElementIds,
...syncedElements,
});
}
return res.render('courses/create-course', {
Expand All @@ -316,18 +344,13 @@ const editCourseHandler = (req, res, next) => {
course,
colors,
classesAndGroups: markSelected(classesAndGroups, classAndGroupIdsOfCourse),
teachers: markSelected(
teachers,
course.teacherIds,
),
substitutions: markSelected(
substitutions,
course.substitutionIds,
),
teachers: markSelected(teachers, course.teacherIds),
substitutions: markSelected(substitutions, course.substitutionIds),
students: filterStudents(res, markSelected(students, course.userIds)),
redirectUrl: req.query.redirectUrl || '/courses',
schoolData: res.locals.currentSchoolData,
pageTitle: res.$t('courses.add.headline.addCourse'),
...syncedElements,
});
}).catch(next);
};
Expand Down Expand Up @@ -546,6 +569,9 @@ router.post('/', (req, res, next) => {
req.body.untilDate = untilDate.toDate();
}

const keys = ['teacherIds', 'substitutionIds', 'classIds', 'userIds'];
req.body = strToPropsArray(req.body, keys);

req.body.features = [];
OPTIONAL_COURSE_FEATURES.forEach((feature) => {
if (req.body[feature] === 'true') {
Expand All @@ -569,6 +595,7 @@ router.post('/', (req, res, next) => {
});

router.get('/add/', editCourseHandler);
router.get('/:groupId/sync', editCourseHandler);

/*
* Single Course
Expand Down Expand Up @@ -792,18 +819,8 @@ router.patch('/:courseId', async (req, res, next) => {
req.body.substitutionIds = [];
}

if (typeof req.body.teacherIds === 'string') {
req.body.teacherIds = req.body.teacherIds.split(',');
}
if (typeof req.body.substitutionIds === 'string') {
req.body.substitutionIds = req.body.substitutionIds.split(',');
}
if (typeof req.body.classIds === 'string') {
req.body.classIds = req.body.classIds.split(',');
}
if (typeof req.body.userIds === 'string') {
req.body.userIds = req.body.userIds.split(',');
}
const keys = ['teacherIds', 'substitutionIds', 'classIds', 'userIds'];
req.body = strToPropsArray(req.body, keys);

const startDate = timesHelper.dateStringToMoment(req.body.startDate);
const untilDate = timesHelper.dateStringToMoment(req.body.untilDate);
Expand Down
6 changes: 6 additions & 0 deletions helpers/handlebars/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ const helpers = () => ({
}
return opts.inverse(this);
},
ifNot: (value, opts) => {
if (!value) {
return opts.fn(this);
}
return opts.inverse(this);
},
ifvalue: (conditional, options) => {
if (options.hash.value === conditional) {
return options.fn(this);
Expand Down
138 changes: 82 additions & 56 deletions views/courses/create-course.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
<input type="hidden" name="_method" data-force-value="true" value="{{method}}"/>
{{/if}}

{{#if syncedWithGroup}}
<input type="hidden" name="syncedWithGroup" data-force-value="true" value="{{syncedWithGroup}}"/>
{{/if}}

<input name="schoolId" type="hidden" data-force-value="true" value="{{currentSchool}}"/>

<div class="row">
Expand Down Expand Up @@ -105,7 +109,7 @@
<div class="form-group col-md-6" data-testid="teachers_container">
<label for="courseTeacher">{{$t "administration.global.label.teachingTeacher"}}</label>
<select id="courseTeacher" name="teacherIds[]" required multiple data-placeholder="{{$t "courses.global.input.chooseTeacher"}}" data-testid="teachersearch" autocomplete="off"
aria-describedby="courseTeacherErr">
aria-describedby="courseTeacherErr" {{#if syncedWithGroup}}disabled{{/if}}>
{{#each teachers}}
<option data-testid="teacher" value="{{this._id}}" {{#if this.selected}}selected{{/if}} {{#if this.isHidden}}hidden{{/if}}>
{{#if this.displayName}}
Expand All @@ -116,14 +120,18 @@
</option>
{{/each}}
</select>
{{#if syncedWithGroup}}
<input type="hidden" id="courseTeacherSync" name="teacherIds" value="{{teachersSelected}}">
{{/if}}
<span class="small course-validation-error" id="courseTeacherErr">
{{$t "courses.global.input.noCourseTeacher"}}</span>
</div>

<div class="form-group col-md-6" data-testid="courseSubstitute_container">
<label for="courseSubstitute">{{$t "courses.add.label.courseSubstitute"}}</label>
<select id="courseSubstitute" name="substitutionIds[]" multiple data-placeholder="{{$t "courses.global.input.chooseTeacher"}}" autocomplete="off" data-testid="substituent">
<option disabled value="{{$t "administration.global.label.noSubTeacherSelection"}}">{{$t "administration.global.label.noSubTeacherSelection"}}</option>
<select id="courseSubstitute" name="substitutionIds[]" multiple data-placeholder="{{$t "courses.global.input.chooseTeacher"}}" autocomplete="off" data-testid="substituent"
{{#if syncedWithGroup}}disabled{{/if}}>
<option disabled value="{{$t "administration.global.label.noSubTeacherSelection"}}">{{$t "administration.global.label.noSubTeacherSelection"}}</option>
{{#each substitutions}}
<option value="{{this._id}}" {{#if this.selected}}selected{{/if}} {{#if this.isHidden}}hidden{{/if}}>
{{#if this.displayName}}
Expand All @@ -134,23 +142,31 @@
</option>
{{/each}}
</select>
{{#if syncedWithGroup}}
<input type="hidden" id="courseSubstituteSync" name="substitutionIds" value="{{substitutionSelected}}">
{{/if}}
</div>
</div>

<div class="form-group">
<div class="times" id="timesContainer">
<div class="row">
<div class="form-group col-md-6">
{{> "lib/forms/form-date-input" label=($t "courses.add.label.timeSpan") id="startDate" class="startDate"
value=course.startDate dataTestId="date_start"}}
<span class="small course-validation-error" id="invalidTimeError">{{$t
"courses.global.input.invalidTimeError"}}</span>
{{> "lib/forms/form-date-input" label=($t "courses.add.label.timeSpan") id="startDate" class="startDate"
value=course.startDate dataTestId="date_start" [email protected]}}
{{#if syncedWithGroup}}
<input type="hidden" id="startDateSync" name="startDate" value="{{startDate}}">
{{/if}}
<span class="small course-validation-error" id="invalidTimeError">
{{$t "courses.global.input.invalidTimeError"}}
</span>
</div>

<div class="form-group col-md-6">
{{> "lib/forms/form-date-input" label=($t "global.label.to") id="untilDate"
class="untilDate"
value=course.untilDate}}
{{> "lib/forms/form-date-input" label=($t "global.label.to") id="untilDate" class="untilDate"
value=course.untilDate [email protected]}}
{{#if syncedWithGroup}}
<input type="hidden" id="untilDateSync" name="untilDate" value="{{untilDate}}">
{{/if}}
</div>
</div>

Expand All @@ -177,55 +193,63 @@
<section data-panel="section-2" data-testid="section-2-area" class="submit-page course-submit-page">
{{#userHasPermission "STUDENT_LIST"}}

<h2 class="h4">{{$t "global.headline.youAlmostMadeIt"}}</h2>
<p class="text-muted">{{$t "courses.add.text.addStudentsOrClasses"}}<br>{{$t "courses.add.text.doItNowOrLaterInSettings"}}</p>
<h2 class="h4">{{$t "global.headline.youAlmostMadeIt"}}</h2>
<p class="text-muted">{{$t "courses.add.text.addStudentsOrClasses"}}<br>{{$t "courses.add.text.doItNowOrLaterInSettings"}}</p>

<div class="form-group" data-testid="class_container">
<label for="addClassesToCourse">{{$t "courses.add.label.addClassesToCourse"}}</label>
<select id="addClassesToCourse" name="classIds[]" multiple data-placeholder="{{$t "courses.global.input.selectClasses"}}" data-testid="classes"
{{#if ../syncedWithGroup}}disabled{{/if}}>
<option disabled value="{{$t "administration.global.placeholder.selectClasses"}}">{{$t "administration.global.placeholder.selectClasses"}}</option>
{{#each ../classesAndGroups}}
<option value="{{this.id}}{{#unless this.id}}{{this._id}}{{/unless}}" {{#if this.selected}}selected{{/if}}>
{{#if (getConfig "FEATURE_GROUPS_IN_COURSE_ENABLED")}}
{{this.name}}

{{#if this.schoolYear}}
({{this.schoolYear}})
{{/if}}

<div class="form-group" data-testid="class_container">
<label for="addClassesToCourse">{{$t "courses.add.label.addClassesToCourse"}}</label>
<select id="addClassesToCourse" name="classIds[]" multiple data-placeholder="{{$t "courses.global.input.selectClasses"}}" data-testid="classes">
<option disabled value="{{$t "administration.global.placeholder.selectClasses"}}">{{$t "administration.global.placeholder.selectClasses"}}</option>
{{#each ../classesAndGroups}}
<option value="{{this.id}}{{#unless this.id}}{{this._id}}{{/unless}}" {{#if this.selected}}selected{{/if}}>
{{#if (getConfig "FEATURE_GROUPS_IN_COURSE_ENABLED")}}
{{#if this.externalSourceName}}
| {{this.externalSourceName}}
{{/if}}
{{else}}
{{#if this.displayName}}
{{this.displayName}}
{{else}}
{{this.name}}
{{/if}}

{{#if this.schoolYear}}
({{this.schoolYear}})
{{/if}}

{{#if this.externalSourceName}}
| {{this.externalSourceName}}
{{/if}}
{{else}}
{{#if this.displayName}}
{{this.displayName}}
{{else}}
{{this.name}}
{{/if}}

{{#if this.year}}
({{this.year.name}})
{{/if}}
{{#if this.year}}
({{this.year.name}})
{{/if}}
</option>
{{/each}}
</select>
</div>
{{/if}}
</option>
{{/each}}
</select>
{{#if ../syncedWithGroup}}
<input type="hidden" id="addClassesToCourseSync" name="classIds" value="{{../classesAndGroupsSelected}}">
{{/if}}
</div>

<div class="form-group" data-testid="students_container">
<label for="addStudentsToCourse">{{$t "courses.add.label.addStudentsToCourse"}}</label>
<select id="addStudentsToCourse" name="userIds[]" multiple data-placeholder="{{$t "courses.global.input.selectStudents"}}" data-testid="pupils">
<option disabled value="{{$t "administration.global.placeholder.selectStudent"}}">{{$t "administration.global.placeholder.selectStudent"}}</option>
{{#each ../students}}
<option value="{{this._id}}" {{#if this.selected}}selected{{/if}} {{#if this.isHidden}}hidden{{/if}}>
{{#if this.displayName}}
{{this.displayName}}
{{else}}
{{this.firstName}} {{this.lastName}}
{{/if}}
</option>
{{/each}}
</select>
<div class="form-group" data-testid="students_container">
<label for="addStudentsToCourse">{{$t "courses.add.label.addStudentsToCourse"}}</label>
<select id="addStudentsToCourse" name="userIds[]" multiple data-placeholder="{{$t "courses.global.input.selectStudents"}}" data-testid="pupils"
{{#if ../syncedWithGroup}}disabled{{/if}}>
<option disabled value="{{$t "administration.global.placeholder.selectStudent"}}">{{$t "administration.global.placeholder.selectStudent"}}</option>
{{#each ../students}}
<option value="{{this._id}}" {{#if this.selected}}selected{{/if}} {{#if this.isHidden}}hidden{{/if}}>
{{#if this.displayName}}
{{this.displayName}}
{{else}}
{{this.firstName}} {{this.lastName}}
{{/if}}
</option>
{{/each}}
</select>
{{#if ../syncedWithGroup}}
<input type="hidden" id="addStudentsToCourseSync" name="userIds" value="{{../studentsSelected}}">
{{/if}}
</div>
{{else}}
<h2 class="h4">{{$t "courses.add.headline.addStudents"}}</h2>
Expand Down Expand Up @@ -254,7 +278,9 @@
</div>

<div class="pull-right">
<a class="btn btn-secondary" data-testid="courses-add-another-course-btn" href="/courses/add">{{$t "courses.add.link.addAnotherCourse"}}</a>
{{#ifNot ../syncedWithGroup}}
<a class="btn btn-secondary" data-testid="courses-add-another-course-btn" href="/courses/add">{{$t "courses.add.link.addAnotherCourse"}}</a>
{{/ifNot}}
<a class="btn btn-primary force-initial-focus" data-testid="courses-to-overview-btn" href="/rooms-overview">{{$t "courses.add.link.courseOverview"}}</a>
</div>
</section>
Expand Down
Loading