diff --git a/assets/vue/components/assignments/AssignmentsForm.vue b/assets/vue/components/assignments/AssignmentsForm.vue
index 4b2cdd4eff8..6169de1df82 100644
--- a/assets/vue/components/assignments/AssignmentsForm.vue
+++ b/assets/vue/components/assignments/AssignmentsForm.vue
@@ -102,6 +102,30 @@
name="allow_text_assignment"
label=""
/>
+
+
+
+
@@ -123,6 +147,7 @@ import BaseAdvancedSettingsButton from "../basecomponents/BaseAdvancedSettingsBu
import BaseButton from "../basecomponents/BaseButton.vue"
import BaseCheckbox from "../basecomponents/BaseCheckbox.vue"
import BaseSelect from "../basecomponents/BaseSelect.vue"
+import BaseMultiSelect from "../basecomponents/BaseMultiSelect.vue";
import BaseInputNumber from "../basecomponents/BaseInputNumber.vue"
import BaseTinyEditor from "../basecomponents/BaseTinyEditor.vue"
import useVuelidate from "@vuelidate/core"
@@ -162,6 +187,17 @@ const documentTypes = ref([
{ label: t("Allow only files"), value: 2 },
])
+const chkRequireExtension = ref(false)
+const predefinedExtensions = ref ([
+ {name: 'PDF', id: 'pdf'},
+ {name: 'DOCX', id: 'docx'},
+ {name: 'XLSX', id: 'xlsx'},
+ {name: 'ZIP', id: 'zip'},
+ {name: 'MP3', id: 'mp3'},
+ {name: 'MP4', id: 'mp4'},
+ {name: t('Other extensions'), id: 'other'},
+])
+
const assignment = reactive({
title: "",
description: "",
@@ -172,6 +208,8 @@ const assignment = reactive({
endsOn: new Date(),
addToCalendar: false,
allowTextAssignment: 2,
+ allowedExtensions: [],
+ customExtensions:'',
})
watchEffect(() => {
@@ -198,13 +236,41 @@ watchEffect(() => {
assignment.allowTextAssignment = def.allowTextAssignment
+
+ if (def.extensions) {
+ const extensionsArray = def.extensions
+ .split(' ')
+ .map(ext => ext.trim())
+ .filter(ext => ext.length > 0)
+
+ if (extensionsArray.length > 0) {
+ chkRequireExtension.value = true
+
+ const predefinedIds = predefinedExtensions.value
+ .map(e => e.id)
+ .filter(id => id !== 'other')
+
+ const predefined = extensionsArray.filter(ext => predefinedIds.includes(ext))
+ const custom = extensionsArray.filter(ext => !predefinedIds.includes(ext))
+ if (assignment.allowedExtensions.length === 0) {
+ assignment.allowedExtensions = predefined
+
+ if (custom.length > 0) {
+ assignment.allowedExtensions.push('other')
+ assignment.customExtensions = custom.join(' ')
+ }
+ }
+ }
+ }
+
if (
def.qualification ||
def.assignment.eventCalendarId ||
def.weight ||
def.assignment.expiresOn ||
def.assignment.endsOn ||
- def.allowTextAssignment !== undefined
+ def.allowTextAssignment !== undefined ||
+ (def.allowedExtensions)
) {
showAdvancedSettings.value = true
}
@@ -256,6 +322,26 @@ async function onSubmit() {
if (chkEndsOn.value) {
payload.endsOn = assignment.endsOn.toISOString()
}
+ if (chkRequireExtension.value && assignment.allowedExtensions.length > 0) {
+ let extensions = []
+
+ assignment.allowedExtensions.forEach(ext => {
+ if (ext !== 'other') {
+ extensions.push(ext)
+ }
+ })
+ if (assignment.allowedExtensions.includes('other') && assignment.customExtensions) {
+ const customExts = assignment.customExtensions
+ .split(' ')
+ .map(ext => ext.trim().toLowerCase().replace('.', ''))
+ .filter(ext => ext.length > 0)
+ extensions.push(...customExts)
+ }
+
+ if (extensions.length > 0) {
+ payload.extensions = extensions.join(' ') // "pdf docx rar ai"
+ }
+ }
if (props.defaultAssignment?.["@id"]) {
payload["@id"] = props.defaultAssignment["@id"]
}
diff --git a/assets/vue/views/assignments/AssignmentSubmit.vue b/assets/vue/views/assignments/AssignmentSubmit.vue
index 8d287dde522..f8c243a10bf 100644
--- a/assets/vue/views/assignments/AssignmentSubmit.vue
+++ b/assets/vue/views/assignments/AssignmentSubmit.vue
@@ -12,6 +12,14 @@
{{ t("Upload your assignment") }} – {{ publicationTitle }}
+
+ {{ t('Allowed file formats:') }}
+ {{ allowedExtensions.map(ext => '.' + ext).join(', ') }}
+
+
ext.trim().toLowerCase())
+ .filter(ext => ext.length > 0) }
} catch (e) {
console.error("Error loading publication metadata", e)
}
}
+function isFileExtensionAllowed(filename) {
+ if (allowedExtensions.value.length === 0) {
+ return true
+ }
+
+ const fileExtension = filename.split('.').pop().toLowerCase()
+ return allowedExtensions.value.includes(fileExtension)
+}
+
+
const queryParams = new URLSearchParams({
cid,
...(sid && { sid }),
@@ -159,6 +184,14 @@ uppy.use(XHRUpload, {
fieldName: "uploadFile",
})
uppy.on("file-added", (file) => {
+ if (!isFileExtensionAllowed(file.name)) {
+ uppy.removeFile(file.id)
+ showErrorNotification(
+ t('File type not allowed. Allowed extensions') + ': ' +
+ allowedExtensions.value.map(ext => '.' + ext).join(', ')
+ )
+ return
+ }
uppy.setMeta({
title: file.name,
filetype: "file",
diff --git a/src/CourseBundle/Entity/CStudentPublication.php b/src/CourseBundle/Entity/CStudentPublication.php
index 352559f2ea4..fc5eef6add5 100644
--- a/src/CourseBundle/Entity/CStudentPublication.php
+++ b/src/CourseBundle/Entity/CStudentPublication.php
@@ -207,6 +207,7 @@ class CStudentPublication extends AbstractResource implements ResourceInterface,
#[ORM\Column(name: 'default_visibility', type: 'boolean', nullable: true, options: ['default' => 0])]
protected ?bool $defaultVisibility = null;
+ #[Groups(['c_student_publication:write', 'student_publication:read'])]
#[ORM\Column(name: 'extensions', type: 'text', nullable: true)]
protected ?string $extensions = null;