Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
88 changes: 87 additions & 1 deletion assets/vue/components/assignments/AssignmentsForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,30 @@
name="allow_text_assignment"
label=""
/>

<BaseCheckbox
id="require_extension"
v-model="chkRequireExtension"
:label="t('Require specific file format')"
name="require_extension"
/>

<div v-if="chkRequireExtension">
<BaseMultiSelect
v-model="assignment.allowedExtensions"
:options="predefinedExtensions"
:label="t('Select allowed file formats')"
input-id="allowed-file-extensions"
/>

<BaseInputText
v-if="assignment.allowedExtensions.includes('other')"
id="custom-extensions"
v-model="assignment.customExtensions"
:label="t('Custom extensions (separated by space)')"
/>

</div>
</BaseAdvancedSettingsButton>

<div class="flex justify-end space-x-2 mt-4">
Expand All @@ -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"
Expand Down Expand Up @@ -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: "",
Expand All @@ -172,6 +208,8 @@ const assignment = reactive({
endsOn: new Date(),
addToCalendar: false,
allowTextAssignment: 2,
allowedExtensions: [],
customExtensions:'',
})

watchEffect(() => {
Expand All @@ -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
}
Expand Down Expand Up @@ -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"]
}
Expand Down
33 changes: 33 additions & 0 deletions assets/vue/views/assignments/AssignmentSubmit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@

<h1 class="text-2xl font-bold">{{ t("Upload your assignment") }} – {{ publicationTitle }}</h1>

<p
v-if="allowedExtensions.length > 0"
class="text-gray-600"
>
<span class="font-semibold">{{ t('Allowed file formats:') }}</span>
{{ allowedExtensions.map(ext => '.' + ext).join(', ') }}
</p>

<div
v-if="allowText && !allowFile"
class="space-y-4"
Expand Down Expand Up @@ -129,6 +137,7 @@ const publicationTitle = ref("")
const text = ref("")
const submissionTitle = ref("")
const activeTab = ref(allowText ? "text" : "file")
const allowedExtensions = ref([])

onMounted(loadPublicationTitle)
async function loadPublicationTitle() {
Expand All @@ -138,11 +147,27 @@ async function loadPublicationTitle() {
})
publicationTitle.value = data.title
submissionTitle.value = data.title

if (data.extensions) {
allowedExtensions.value = data.extensions
.split(' ')
.map(ext => 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 }),
Expand All @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/CourseBundle/Entity/CStudentPublication.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Loading