Skip to content

Commit cb59270

Browse files
committed
Merge branch 'master' of github.com:chamilo/chamilo-lms
2 parents 37a4626 + 992626f commit cb59270

File tree

23 files changed

+1168
-198
lines changed

23 files changed

+1168
-198
lines changed

assets/css/scss/molecules/_course_tool.scss

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@
1111
}
1212

1313
&__shadow {
14-
@apply absolute w-16 h-16 text-support-1;
14+
@apply absolute w-16 h-16 text-primary;
15+
16+
padding: 1px;
17+
opacity: 0.08;
1518
}
1619

1720
&__icon {
1821
@apply text-transparent bg-clip-text bg-gradient-to-br from-primary to-primary-gradient leading-none;
1922

2023
&.mdi {
21-
font-size: 52px;
24+
font-size: 44px;
2225
}
2326
}
2427

assets/vue/components/admin/ColorThemePreview.vue

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,20 @@ provide("isCustomizing", isCustomizing)
220220
</div>
221221
<div>
222222
<p class="mb-3 text-lg">{{ t("Some more elements") }}</p>
223+
223224
<div class="course-tool cursor-pointer">
224225
<div class="course-tool__link hover:primary-gradient hover:bg-primary-gradient/10">
226+
<svg
227+
class="course-tool__shadow"
228+
fill="none"
229+
viewBox="0 0 117 105"
230+
xmlns="http://www.w3.org/2000/svg"
231+
>
232+
<path
233+
class="fill-current"
234+
d="M104.167 20.2899C104.167 43.3465 116.11 59.1799 116.11 70.2899C116.11 81.3999 109.723 104.733 58.6133 104.733C7.50333 104.733 0 73.3432 0 61.1232C0 3.89987 104.167 -20.5435 104.167 20.2899Z"
235+
/>
236+
</svg>
225237
<span
226238
aria-hidden="true"
227239
class="course-tool__icon mdi mdi-bookshelf"

assets/vue/components/assignments/AssignmentsForm.vue

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,30 @@
102102
name="allow_text_assignment"
103103
label=""
104104
/>
105+
106+
<BaseCheckbox
107+
id="require_extension"
108+
v-model="chkRequireExtension"
109+
:label="t('Require specific file format')"
110+
name="require_extension"
111+
/>
112+
113+
<div v-if="chkRequireExtension">
114+
<BaseMultiSelect
115+
v-model="assignment.allowedExtensions"
116+
:options="predefinedExtensions"
117+
:label="t('Select allowed file formats')"
118+
input-id="allowed-file-extensions"
119+
/>
120+
121+
<BaseInputText
122+
v-if="assignment.allowedExtensions.includes('other')"
123+
id="custom-extensions"
124+
v-model="assignment.customExtensions"
125+
:label="t('Custom extensions (separated by space)')"
126+
/>
127+
128+
</div>
105129
</BaseAdvancedSettingsButton>
106130

107131
<div class="flex justify-end space-x-2 mt-4">
@@ -123,6 +147,7 @@ import BaseAdvancedSettingsButton from "../basecomponents/BaseAdvancedSettingsBu
123147
import BaseButton from "../basecomponents/BaseButton.vue"
124148
import BaseCheckbox from "../basecomponents/BaseCheckbox.vue"
125149
import BaseSelect from "../basecomponents/BaseSelect.vue"
150+
import BaseMultiSelect from "../basecomponents/BaseMultiSelect.vue";
126151
import BaseInputNumber from "../basecomponents/BaseInputNumber.vue"
127152
import BaseTinyEditor from "../basecomponents/BaseTinyEditor.vue"
128153
import useVuelidate from "@vuelidate/core"
@@ -162,6 +187,17 @@ const documentTypes = ref([
162187
{ label: t("Allow only files"), value: 2 },
163188
])
164189
190+
const chkRequireExtension = ref(false)
191+
const predefinedExtensions = ref ([
192+
{name: 'PDF', id: 'pdf'},
193+
{name: 'DOCX', id: 'docx'},
194+
{name: 'XLSX', id: 'xlsx'},
195+
{name: 'ZIP', id: 'zip'},
196+
{name: 'MP3', id: 'mp3'},
197+
{name: 'MP4', id: 'mp4'},
198+
{name: t('Other extensions'), id: 'other'},
199+
])
200+
165201
const assignment = reactive({
166202
title: "",
167203
description: "",
@@ -172,6 +208,8 @@ const assignment = reactive({
172208
endsOn: new Date(),
173209
addToCalendar: false,
174210
allowTextAssignment: 2,
211+
allowedExtensions: [],
212+
customExtensions:'',
175213
})
176214
177215
watchEffect(() => {
@@ -198,13 +236,41 @@ watchEffect(() => {
198236
199237
assignment.allowTextAssignment = def.allowTextAssignment
200238
239+
240+
if (def.extensions) {
241+
const extensionsArray = def.extensions
242+
.split(' ')
243+
.map(ext => ext.trim())
244+
.filter(ext => ext.length > 0)
245+
246+
if (extensionsArray.length > 0) {
247+
chkRequireExtension.value = true
248+
249+
const predefinedIds = predefinedExtensions.value
250+
.map(e => e.id)
251+
.filter(id => id !== 'other')
252+
253+
const predefined = extensionsArray.filter(ext => predefinedIds.includes(ext))
254+
const custom = extensionsArray.filter(ext => !predefinedIds.includes(ext))
255+
if (assignment.allowedExtensions.length === 0) {
256+
assignment.allowedExtensions = predefined
257+
258+
if (custom.length > 0) {
259+
assignment.allowedExtensions.push('other')
260+
assignment.customExtensions = custom.join(' ')
261+
}
262+
}
263+
}
264+
}
265+
201266
if (
202267
def.qualification ||
203268
def.assignment.eventCalendarId ||
204269
def.weight ||
205270
def.assignment.expiresOn ||
206271
def.assignment.endsOn ||
207-
def.allowTextAssignment !== undefined
272+
def.allowTextAssignment !== undefined ||
273+
(def.allowedExtensions)
208274
) {
209275
showAdvancedSettings.value = true
210276
}
@@ -256,6 +322,26 @@ async function onSubmit() {
256322
if (chkEndsOn.value) {
257323
payload.endsOn = assignment.endsOn.toISOString()
258324
}
325+
if (chkRequireExtension.value && assignment.allowedExtensions.length > 0) {
326+
let extensions = []
327+
328+
assignment.allowedExtensions.forEach(ext => {
329+
if (ext !== 'other') {
330+
extensions.push(ext)
331+
}
332+
})
333+
if (assignment.allowedExtensions.includes('other') && assignment.customExtensions) {
334+
const customExts = assignment.customExtensions
335+
.split(' ')
336+
.map(ext => ext.trim().toLowerCase().replace('.', ''))
337+
.filter(ext => ext.length > 0)
338+
extensions.push(...customExts)
339+
}
340+
341+
if (extensions.length > 0) {
342+
payload.extensions = extensions.join(' ') // "pdf docx rar ai"
343+
}
344+
}
259345
if (props.defaultAssignment?.["@id"]) {
260346
payload["@id"] = props.defaultAssignment["@id"]
261347
}

assets/vue/components/course/CourseCard.vue

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ import { useI18n } from "vue-i18n"
9393
import { useCourseRequirementStatus } from "../../composables/course/useCourseRequirementStatus"
9494
import BaseButton from "../basecomponents/BaseButton.vue"
9595
import CatalogueRequirementModal from "./CatalogueRequirementModal.vue"
96+
import { useUserSessionSubscription } from "../../composables/userPermissions"
9697
9798
const { abbreviatedDatetime } = useFormatDate()
9899
@@ -120,25 +121,66 @@ const props = defineProps({
120121
121122
const { t } = useI18n()
122123
const platformConfigStore = usePlatformConfig()
124+
const { isCoach } = useUserSessionSubscription(props.session, props.course)
125+
123126
const showRemainingDays = computed(
124127
() => platformConfigStore.getSetting("session.session_list_view_remaining_days") === "true",
125128
)
126129
127130
const daysRemainingText = computed(() => {
128-
if (!showRemainingDays.value || !props.session?.displayEndDate) return null
131+
if (!showRemainingDays.value || !props.session?.displayEndDate) {
132+
return null
133+
}
129134
130135
const endDate = new Date(props.session.displayEndDate)
131-
if (isNaN(endDate)) return null
136+
if (isNaN(endDate)) {
137+
return null
138+
}
132139
133140
const today = new Date()
134141
const diff = Math.floor((endDate - today) / (1000 * 60 * 60 * 24))
135142
136-
if (diff > 1) return `${diff} days remaining`
137-
if (diff === 1) return t("Ends tomorrow")
138-
if (diff === 0) return t("Ends today")
143+
if (diff > 1) {
144+
return `${diff} days remaining`
145+
}
146+
if (diff === 1) {
147+
return t("Ends tomorrow")
148+
}
149+
if (diff === 0) {
150+
return t("Ends today")
151+
}
152+
139153
return t("Expired")
140154
})
141155
156+
const sessionDurationText = computed(() => {
157+
// Only show duration in days when the setting is enabled and the user is a coach
158+
if (!showRemainingDays.value || !isCoach.value) {
159+
return null
160+
}
161+
162+
if (!props.session?.displayStartDate || !props.session?.displayEndDate) {
163+
return null
164+
}
165+
166+
const start = new Date(props.session.displayStartDate)
167+
const end = new Date(props.session.displayEndDate)
168+
169+
if (isNaN(start) || isNaN(end)) {
170+
return null
171+
}
172+
173+
const msPerDay = 1000 * 60 * 60 * 24
174+
const rawDiff = Math.floor((end - start) / msPerDay) + 1
175+
const days = rawDiff > 0 ? rawDiff : 1
176+
177+
if (days === 1) {
178+
return "1 day duration"
179+
}
180+
181+
return `${days} days duration`
182+
})
183+
142184
const showCourseDuration = computed(() => platformConfigStore.getSetting("course.show_course_duration") === "true")
143185
144186
const teachers = computed(() => {
@@ -159,11 +201,23 @@ const teachers = computed(() => {
159201
})
160202
161203
const sessionDisplayDate = computed(() => {
162-
if (daysRemainingText.value) return daysRemainingText.value
204+
// When setting is enabled, decide between duration (for coaches) and remaining days (for regular users)
205+
if (sessionDurationText.value) {
206+
return sessionDurationText.value
207+
}
208+
209+
if (daysRemainingText.value) {
210+
return daysRemainingText.value
211+
}
163212
213+
// Fallback: show the original date range
164214
const parts = []
165-
if (props.session?.displayStartDate) parts.push(abbreviatedDatetime(props.session.displayStartDate))
166-
if (props.session?.displayEndDate) parts.push(abbreviatedDatetime(props.session.displayEndDate))
215+
if (props.session?.displayStartDate) {
216+
parts.push(abbreviatedDatetime(props.session.displayStartDate))
217+
}
218+
if (props.session?.displayEndDate) {
219+
parts.push(abbreviatedDatetime(props.session.displayEndDate))
220+
}
167221
168222
return parts.join("")
169223
})

0 commit comments

Comments
 (0)