Skip to content

Commit 400f7a4

Browse files
committed
Admin: Add update courses through CSV/XML - refs BT#21441
1 parent 80df5b7 commit 400f7a4

File tree

3 files changed

+356
-0
lines changed

3 files changed

+356
-0
lines changed

main/admin/course_update_import.php

+345
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
<?php
2+
/* For licensing terms, see /license.txt */
3+
4+
$cidReset = true;
5+
6+
require_once __DIR__.'/../inc/global.inc.php';
7+
api_protect_admin_script();
8+
9+
/**
10+
* Generates a CSV model string showing how the CSV file should be structured for course updates.
11+
*/
12+
function generateCsvModel(array $fields): string
13+
{
14+
$headerCsv = "<strong>Code</strong>;Title;CourseCategory;Language;";
15+
16+
$exampleCsv = "<b>COURSE001</b>;Introduction to Biology;BIO;english;";
17+
18+
foreach ($fields as $field) {
19+
$fieldType = (int) $field['field_type'];
20+
switch ($fieldType) {
21+
case ExtraField::FIELD_TYPE_CHECKBOX:
22+
$exampleValue = '1'; // 1 for true, 0 for false
23+
break;
24+
case ExtraField::FIELD_TYPE_TAG:
25+
$exampleValue = 'tag1,tag2,tag3'; // Comma separated list of tags
26+
break;
27+
default:
28+
$exampleValue = 'xxx'; // Example value for text fields
29+
}
30+
31+
$headerCsv .= "<span style=\"color:red;\">".$field['field_variable']."</span>;";
32+
33+
$exampleCsv .= "<span style=\"color:red;\">$exampleValue</span>;";
34+
}
35+
36+
$modelCsv = $headerCsv."\n".$exampleCsv;
37+
38+
return $modelCsv;
39+
}
40+
41+
/**
42+
* Generates an XML model string showing how the XML file should be structured for course updates.
43+
*/
44+
function generateXmlModel(array $fields): string
45+
{
46+
$modelXml = "&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt;\n";
47+
$modelXml .= "&lt;Courses&gt;\n";
48+
$modelXml .= " &lt;Course&gt;\n";
49+
$modelXml .= " <b>&lt;Code&gt;COURSE001&lt;/Code&gt;</b>\n";
50+
$modelXml .= " &lt;Title&gt;Introduction to Biology&lt;/Title&gt;\n";
51+
$modelXml .= " &lt;CourseCategory&gt;BIO&lt;/CourseCategory&gt;\n";
52+
$modelXml .= " &lt;Language&gt;english&lt;/Language&gt;\n";
53+
foreach ($fields as $field) {
54+
switch ($field['field_type']) {
55+
case ExtraField::FIELD_TYPE_CHECKBOX:
56+
$exampleValue = '1'; // 1 for true, 0 for false
57+
break;
58+
case ExtraField::FIELD_TYPE_TAG:
59+
$exampleValue = 'tag1,tag2,tag3'; // Comma separated list of tags
60+
break;
61+
default:
62+
$exampleValue = 'xxx'; // Example value for text fields
63+
}
64+
65+
$modelXml .= " <span style=\"color:red;\">&lt;".$field['field_variable']."&gt;$exampleValue&lt;/".$field['field_variable']."&gt;</span>\n";
66+
}
67+
$modelXml .= " &lt;/Course&gt;\n";
68+
$modelXml .= "&lt;/Courses&gt;";
69+
70+
return $modelXml;
71+
}
72+
73+
/**
74+
* Function to validate course data from the CSV/XML file.
75+
*/
76+
function validateCourseData(array $courses): array
77+
{
78+
$errors = [];
79+
$courseCodes = [];
80+
81+
foreach ($courses as $course) {
82+
if (empty($course['Code'])) {
83+
$errors[] = get_lang("CodeIsRequired");
84+
} else {
85+
$courseId = api_get_course_int_id($course['Code']);
86+
if (!$courseId) {
87+
$errors[] = get_lang("CourseCodeDoesNotExist").': '.$course['Code'];
88+
} elseif (in_array($course['Code'], $courseCodes)) {
89+
$errors[] = get_lang("DuplicateCode").': '.$course['Code'];
90+
}
91+
92+
$courseCodes[] = $course['Code'];
93+
}
94+
}
95+
96+
return $errors;
97+
}
98+
99+
/**
100+
* Update course data in the database.
101+
*/
102+
function updateCourse(array $courseData, int $courseId): void
103+
{
104+
$courseTable = Database::get_main_table(TABLE_MAIN_COURSE);
105+
$params = [
106+
'title' => $courseData['Title'],
107+
'course_language' => $courseData['Language'],
108+
'category_code' => $courseData['CourseCategory'],
109+
'visual_code' => $courseData['Code'],
110+
];
111+
Database::update($courseTable, $params, ['id = ?' => $courseId]);
112+
$courseData['code'] = $courseData['Code'];
113+
$courseData['item_id'] = $courseId;
114+
$courseFieldValue = new ExtraFieldValue('course');
115+
$courseFieldValue->saveFieldValues($courseData);
116+
}
117+
118+
/**
119+
* Function to update courses from the imported data.
120+
*/
121+
function updateCourses(array $courses): void
122+
{
123+
foreach ($courses as $course) {
124+
$courseId = api_get_course_int_id($course['Code']);
125+
updateCourse($course, $courseId);
126+
}
127+
}
128+
129+
/**
130+
* Function to parse CSV data.
131+
*/
132+
function parseCsvCourseData(string $file, array $extraFields): array
133+
{
134+
$data = Import::csv_reader($file);
135+
$courses = [];
136+
137+
foreach ($data as $row) {
138+
$courseData = [];
139+
foreach ($row as $key => $value) {
140+
if (empty($key)) {
141+
continue;
142+
}
143+
if (in_array($key, array_column($extraFields, 'variable'))) {
144+
$processedValue = processExtraFieldValue($key, $value, $extraFields);
145+
$courseData['extra_'.$key] = $processedValue;
146+
} else {
147+
$courseData[$key] = $value;
148+
}
149+
}
150+
151+
$courses[] = $courseData;
152+
}
153+
154+
return $courses;
155+
}
156+
157+
/**
158+
* Function to parse XML data.
159+
*/
160+
function parseXmlCourseData(string $file, array $extraFields): array
161+
{
162+
$xmlContent = Import::xml($file);
163+
$courses = [];
164+
165+
foreach ($xmlContent->filter('Courses > Course') as $xmlCourse) {
166+
$courseData = [];
167+
foreach ($xmlCourse->childNodes as $node) {
168+
if ($node->nodeName !== '#text') {
169+
$key = $node->nodeName;
170+
if (empty($key)) {
171+
continue;
172+
}
173+
$value = $node->nodeValue;
174+
if (in_array($key, array_column($extraFields, 'variable'))) {
175+
$processedValue = processExtraFieldValue($key, $value, $extraFields);
176+
$courseData['extra_'.$key] = $processedValue;
177+
} else {
178+
$courseData[$key] = $value;
179+
}
180+
}
181+
}
182+
183+
if (!empty($courseData)) {
184+
$courses[] = $courseData;
185+
}
186+
}
187+
188+
return $courses;
189+
}
190+
191+
/**
192+
* Processes the value of an extra field based on its type.
193+
*
194+
* This function takes the name and value of an extra field, along with an array of all extra fields, and processes
195+
* the value according to the field type. For checkbox fields, it returns an array with the field name as the key
196+
* and '1' (checked) or '0' (unchecked) as the value. For tag fields, it splits the string by commas into an array.
197+
* For other types, it returns the value as is.
198+
*/
199+
function processExtraFieldValue(string $fieldName, $value, array $extraFields)
200+
{
201+
$fieldIndex = array_search($fieldName, array_column($extraFields, 'variable'));
202+
if ($fieldIndex === false) {
203+
return $value;
204+
}
205+
206+
$fieldType = $extraFields[$fieldIndex]['field_type'];
207+
208+
switch ($fieldType) {
209+
case ExtraField::FIELD_TYPE_CHECKBOX:
210+
$newValue = 0;
211+
if ($value == '1') {
212+
$newValue = ['extra_'.$fieldName => '1'];
213+
}
214+
return $newValue;
215+
case ExtraField::FIELD_TYPE_TAG:
216+
return explode(',', $value);
217+
default:
218+
return $value;
219+
}
220+
}
221+
222+
$toolName = get_lang('UpdateCourseListXMLCSV');
223+
$interbreadcrumb[] = ["url" => 'index.php', "name" => get_lang('PlatformAdmin')];
224+
225+
$form = new FormValidator('course_update_import');
226+
$form->addHeader(get_lang('UpdateCourseListXMLCSV'));
227+
$form->addFile('importFile', get_lang('ImportCSVFileLocation'));
228+
229+
$form->addElement('radio', 'file_type', get_lang('FileType'), get_lang('CSV'), 'csv');
230+
$form->addElement('radio', 'file_type', '', get_lang('XML'), 'xml');
231+
232+
$defaults['file_type'] = 'csv';
233+
$form->setDefaults($defaults);
234+
235+
$form->addButtonImport(get_lang('Import'));
236+
237+
if ($form->validate()) {
238+
if (!isset($_FILES['importFile']['error']) || is_array($_FILES['importFile']['error'])) {
239+
Display::addFlash(Display::return_message(get_lang('InvalidFileUpload'), 'error'));
240+
} else {
241+
switch ($_FILES['importFile']['error']) {
242+
case UPLOAD_ERR_OK:
243+
break;
244+
case UPLOAD_ERR_NO_FILE:
245+
Display::addFlash(Display::return_message(get_lang('NoFileSent'), 'error'));
246+
break;
247+
case UPLOAD_ERR_INI_SIZE:
248+
case UPLOAD_ERR_FORM_SIZE:
249+
Display::addFlash(Display::return_message(get_lang('ExceededFileSizeLimit'), 'error'));
250+
break;
251+
default:
252+
Display::addFlash(Display::return_message(get_lang('UnknownErrors'), 'error'));
253+
}
254+
}
255+
256+
$fileType = $_POST['file_type'];
257+
$fileExt = strtolower(pathinfo($_FILES['importFile']['name'], PATHINFO_EXTENSION));
258+
259+
if (($fileType === 'csv' && $fileExt !== 'csv') || ($fileType === 'xml' && $fileExt !== 'xml')) {
260+
Display::addFlash(Display::return_message(get_lang('InvalidFileType'), 'error'));
261+
} else {
262+
$file = $_FILES['importFile']['tmp_name'];
263+
$extraField = new ExtraField('course');
264+
$allExtraFields = $extraField->get_all();
265+
$successfulUpdates = [];
266+
$failedUpdates = [];
267+
try {
268+
if ($fileType === 'csv') {
269+
$courses = parseCsvCourseData($file, $allExtraFields);
270+
} else {
271+
$courses = parseXmlCourseData($file, $allExtraFields);
272+
}
273+
274+
foreach ($courses as $course) {
275+
$courseErrors = validateCourseData([$course]);
276+
if (!empty($courseErrors)) {
277+
$failedUpdates[] = $course['Code'].': '.implode(', ', $courseErrors);
278+
continue;
279+
}
280+
try {
281+
updateCourses([$course]);
282+
$successfulUpdates[] = $course['Code'];
283+
} catch (Exception $e) {
284+
$failedUpdates[] = $course['Code'].': '.$e->getMessage();
285+
}
286+
}
287+
288+
if (!empty($successfulUpdates)) {
289+
Display::addFlash(Display::return_message(get_lang('CoursesUpdatedSuccessfully').': '.implode(', ', $successfulUpdates), 'success'));
290+
}
291+
292+
if (!empty($failedUpdates)) {
293+
foreach ($failedUpdates as $error) {
294+
Display::addFlash(Display::return_message(get_lang('UpdateFailedForCourses').': '.$error, 'error'));
295+
}
296+
}
297+
} catch (Exception $e) {
298+
Display::addFlash(Display::return_message($e->getMessage(), 'error'));
299+
}
300+
}
301+
}
302+
303+
$htmlHeadXtra[] = "<script>
304+
$(document).ready(function() {
305+
function showFileType(type) {
306+
if (type === 'csv') {
307+
$('#csv-model').show();
308+
$('#xml-model').hide();
309+
} else {
310+
$('#csv-model').hide();
311+
$('#xml-model').show();
312+
}
313+
}
314+
315+
showFileType($('input[name=file_type]:checked').val());
316+
317+
$('input[name=file_type]').on('change', function() {
318+
showFileType($(this).val());
319+
});
320+
});
321+
</script>";
322+
323+
Display::display_header($toolName);
324+
325+
$form->display();
326+
327+
$extraField = new ExtraField('course');
328+
$allExtraFields = $extraField->get_all();
329+
330+
$extraFields = [];
331+
foreach ($allExtraFields as $field) {
332+
$extraFields[] = [
333+
'field_variable' => $field['variable'],
334+
'field_type' => $field['field_type'],
335+
];
336+
}
337+
338+
$csvContent = generateCsvModel($extraFields);
339+
$xmlContent = generateXmlModel($extraFields);
340+
echo '<div id="csv-model"><p>' . get_lang('CSVMustLookLike') . ' (' . get_lang('MandatoryFields') . '):</p>';
341+
echo '<blockquote><pre>' . $csvContent . '</pre></blockquote></div>';
342+
echo '<div id="xml-model" style="display: none;"><p>' . get_lang('XMLMustLookLike') . ' (' . get_lang('MandatoryFields') . '):</p>';
343+
echo '<blockquote><pre>' . $xmlContent . '</pre></blockquote></div>';
344+
345+
Display::display_footer();

main/admin/index.php

+5
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,11 @@
292292
'url' => 'course_import.php',
293293
'label' => get_lang('ImportCourses'),
294294
];
295+
$items[] = [
296+
'class' => 'item-course-import-update',
297+
'url' => 'course_update_import.php',
298+
'label' => get_lang('UpdateCourseListXMLCSV'),
299+
];
295300
$items[] = [
296301
'class' => 'item-course-category',
297302
'url' => 'course_category.php',

main/lang/english/trad4all.inc.php

+6
Original file line numberDiff line numberDiff line change
@@ -9046,4 +9046,10 @@
90469046
$CopyIframeCodeToIncludeExercise = "Copy iframe code below to include the exercise :";
90479047
$MyMissingSignatures = "My missing signatures";
90489048
$OnlyShowActiveUsers = "Only show active users";
9049+
$UpdateCourseListXMLCSV = "Update courses list";
9050+
$CodeIsRequired = "A code is required";
9051+
$CourseCodeDoesNotExist = "This course code does not exist";
9052+
$DuplicateCode = "Duplicate code";
9053+
$CoursesUpdatedSuccessfully = "Courses updated successfully";
9054+
$UpdateFailedForCourses = "The update failed for the following courses";
90499055
?>

0 commit comments

Comments
 (0)