Skip to content

Commit 481430a

Browse files
committed
Merge branch 'main-hotfix' of https://github.com/frappe/lms into sync/main-hotfix-to-main-v2
# Conflicts: # lms/patches.txt
2 parents 1c3b2e7 + f3fd3b5 commit 481430a

15 files changed

Lines changed: 471 additions & 257 deletions

File tree

.github/semgrep/tailwind-rtl.yml

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,46 @@
1-
# Local run: semgrep scan --config .github/semgrep/tailwind-rtl.yml --metrics off .
1+
# Local run: semgrep scan --config .github/semgrep/tailwind-rtl.yml
22
rules:
33
- id: tailwind-physical-class
44
message: |
55
RTL: replace physical Tailwind class with logical equivalent.
66
languages: [generic]
77
severity: WARNING
88
paths: { include: ["*.vue"] }
9-
pattern-regex: |
10-
(?x)
11-
(?:^|[\s'"`{(\[,]) !?(?:[a-z][a-z0-9-]*:)*-?
12-
(?:
13-
(?:scroll-)?[mp][lr]-(?:auto|px|full|\d+(?:[./]\d+)?|\[[^\]]+\])
14-
| (?:left|right)-(?:auto|px|full|\d+(?:[./]\d+)?|\[[^\]]+\])
15-
| text-(?:left|right)
16-
| float-(?:left|right)
17-
| clear-(?:left|right)
18-
| (?:border|rounded)-[lr](?:-[A-Za-z0-9\[\]./#%_-]+)?
19-
| rounded-(?:tl|tr|bl|br)(?:-[A-Za-z0-9\[\]./#%_-]+)?
20-
| space-x-(?:auto|px|reverse|\d+(?:[./]\d+)?|\[[^\]]+\])
21-
)
22-
!? (?=$|[\s'"`})\],>;])
9+
patterns:
10+
- pattern-either:
11+
- pattern-regex: |
12+
(?x)
13+
(?:^|[\s"'`(\[{,])
14+
!?
15+
(?:[^\s"':]+:)*
16+
-?
17+
(?:
18+
(?:scroll-)?[mp][lr]-(?:auto|px|full|\d+(?:[./]\d+)?|\[[^\]]+\])
19+
| (?:left|right)-(?:auto|px|full|\d+(?:[./]\d+)?|\[[^\]]+\])
20+
| (?:text|float|clear)-(?:left|right)
21+
| (?:border|rounded)-[lr](?:-[A-Za-z0-9\[\]./#%_-]+)?
22+
| rounded-(?:tl|tr|bl|br)(?:-[A-Za-z0-9\[\]./#%_-]+)?
23+
)
24+
!?
25+
(?=$|[\s'"`})\],>;])
26+
- pattern-not-regex: '\b(?:rtl|ltr):'
27+
28+
- id: tailwind-space-x-needs-reverse-or-gap
29+
message: |
30+
RTL: `space-x-*` does not flip in RTL. add `rtl:space-x-reverse` or `gap-x-* ms-*`
31+
languages: [generic]
32+
severity: WARNING
33+
paths: { include: ["*.vue"] }
34+
patterns:
35+
- pattern-regex: 'class\s*[:=]\s*"[^"]*\bspace-x-\S+[^"]*"'
36+
- pattern-not-regex: '\brtl:space-x-reverse\b'
37+
38+
- id: tailwind-divide-x-needs-reverse
39+
message: |
40+
RTL: `divide-x-*` does not flip in RTL, sdd `rtl:divide-x-reverse`.
41+
languages: [generic]
42+
severity: WARNING
43+
paths: { include: ["*.vue"] }
44+
patterns:
45+
- pattern-regex: 'class\s*[:=]\s*"[^"]*\bdivide-x(?:-\S+)?[^"]*"'
46+
- pattern-not-regex: '\brtl:divide-x-reverse\b'

frontend/src/components/CourseCardOverlay.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ const is_instructor = () => {
230230
const canGetCertificate = computed(() => {
231231
if (
232232
props.course.data?.enable_certification &&
233-
props.course.data?.membership?.progress == 100
233+
props.course.data?.membership?.progress >= 100
234234
) {
235235
return true
236236
}

frontend/src/components/Layouts/MobileLayout.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<div class="relative flex h-screen flex-col">
33
<div
4-
class="flex flex-1 flex-col overflow-hidden pb-10"
4+
class="flex flex-1 flex-col overflow-y-auto pb-10"
55
id="scrollContainer"
66
>
77
<slot />

frontend/src/pages/Batches/components/NewBatchModal.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
120120
import { useOnboarding, useTelemetry } from 'frappe-ui/frappe'
121121
import { computed, inject, onMounted, onBeforeUnmount, ref } from 'vue'
122122
import { useRouter } from 'vue-router'
123-
import { sanitizeHTML, createLMSCategory } from '@/utils'
123+
import { sanitizeHTML, createLMSCategory, cleanError } from '@/utils'
124124
import MultiSelect from '@/components/Controls/MultiSelect.vue'
125125
import Link from '@/components/Controls/Link.vue'
126126
import NewMemberModal from '@/components/Modals/NewMemberModal.vue'
@@ -214,7 +214,8 @@ const saveBatch = (close: () => void = () => {}) => {
214214
}
215215
},
216216
onError(err: any) {
217-
toast.error(cleanError(err.messages?.[0]))
217+
const message = err?.messages?.[0]
218+
toast.error(message ? cleanError(message) : __('Error creating batch'))
218219
console.error(err)
219220
},
220221
}

frontend/src/pages/JobApplications.vue

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -209,21 +209,27 @@ const props = defineProps({
209209
210210
const applications = createListResource({
211211
doctype: 'LMS Job Application',
212-
fields: [
213-
'name',
214-
'user.user_image as user_image',
215-
'user.full_name as full_name',
216-
'user.email as email',
217-
'creation',
218-
'resume',
219-
'job.job_title as job_title',
220-
],
212+
fields: ['name', 'user', 'creation', 'resume', 'job_title'],
221213
filters: {
222214
job: props.job,
223215
},
224216
auto: true,
225217
})
226218
219+
const users = createResource({
220+
url: 'lms.lms.api.get_application_users',
221+
makeParams: () => ({
222+
user_names: (applications.data || []).map((a) => a.user),
223+
}),
224+
})
225+
226+
watch(
227+
() => applications.data,
228+
(rows) => {
229+
if (rows?.length) users.submit()
230+
}
231+
)
232+
227233
const totalApplications = createResource({
228234
url: 'frappe.client.get_count',
229235
params: {
@@ -354,11 +360,17 @@ const applicationColumns = computed(() => {
354360
355361
const applicantRows = computed(() => {
356362
if (!applications.data) return []
357-
return applications.data.map((application) => ({
358-
...application,
359-
full_name: application.full_name,
360-
applied_on: dayjs(application.creation).format('DD MMM YYYY'),
361-
}))
363+
const userMap = Object.fromEntries((users.data || []).map((u) => [u.name, u]))
364+
return applications.data.map((application) => {
365+
const user = userMap[application.user] || {}
366+
return {
367+
...application,
368+
user_image: user.user_image,
369+
full_name: user.full_name,
370+
email: user.email,
371+
applied_on: dayjs(application.creation).format('DD MMM YYYY'),
372+
}
373+
})
362374
})
363375
364376
usePageMeta(() => {

lms/lms/api.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,31 @@ def get_job_details(job: str):
231231
)
232232

233233

234+
@frappe.whitelist()
235+
def get_application_users(user_names: list | str):
236+
# temp function workaround:reverting once upstream restores dotted-field JOINs in `frappe.client.get_list`
237+
if isinstance(user_names, str):
238+
user_names = json.loads(user_names)
239+
if not user_names:
240+
return []
241+
242+
visible = frappe.get_list(
243+
"LMS Job Application",
244+
filters={"user": ["in", user_names]},
245+
fields=["user"],
246+
pluck="user",
247+
)
248+
visible_user_names = list(set(visible))
249+
if not visible_user_names:
250+
return []
251+
252+
return frappe.get_all(
253+
"User",
254+
filters={"name": ["in", visible_user_names]},
255+
fields=["name", "user_image", "full_name", "email"],
256+
)
257+
258+
234259
def sanitize_job_filters(filters, or_filters):
235260
ALLOWED_FILTERS = ("status", "type", "work_mode", "country")
236261
ALLOWED_OR_FILTERS = ("job_title", "company_name", "location")
@@ -392,6 +417,7 @@ def get_certified_participants(
392417
for participant in participants:
393418
details = get_certified_participant_details(participant.member)
394419
participant.update(details)
420+
participant.pop("member", None)
395421

396422
return participants
397423

lms/lms/doctype/course_lesson/course_lesson.py

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -98,25 +98,32 @@ def save_progress(lesson: str, course: str, scorm_details: dict = None):
9898
scorm_details = frappe._dict(**scorm_details)
9999

100100
if not progress_already_exists and quiz_completed and assignment_completed and not scorm_details:
101-
frappe.get_doc(
102-
{
103-
"doctype": "LMS Course Progress",
104-
"lesson": lesson,
105-
"status": "Complete",
106-
"member": frappe.session.user,
107-
}
108-
).save(ignore_permissions=True)
101+
try:
102+
frappe.get_doc(
103+
{
104+
"doctype": "LMS Course Progress",
105+
"lesson": lesson,
106+
"status": "Complete",
107+
"member": frappe.session.user,
108+
}
109+
).save(ignore_permissions=True)
110+
except frappe.UniqueValidationError:
111+
# concurrent request created the progress doc
112+
pass
109113
elif scorm_details and not lesson_already_completed and not progress_already_exists:
110114
# Create new SCORM progress
111-
frappe.get_doc(
112-
{
113-
"doctype": "LMS Course Progress",
114-
"lesson": lesson,
115-
"status": "Complete" if scorm_details.is_complete else "Partially Complete",
116-
"member": frappe.session.user,
117-
"scorm_content": "" if scorm_details.is_complete else scorm_details.scorm_content,
118-
}
119-
).save(ignore_permissions=True)
115+
try:
116+
frappe.get_doc(
117+
{
118+
"doctype": "LMS Course Progress",
119+
"lesson": lesson,
120+
"status": "Complete" if scorm_details.is_complete else "Partially Complete",
121+
"member": frappe.session.user,
122+
"scorm_content": "" if scorm_details.is_complete else scorm_details.scorm_content,
123+
}
124+
).save(ignore_permissions=True)
125+
except frappe.UniqueValidationError:
126+
pass
120127
elif scorm_details and not lesson_already_completed and progress_already_exists:
121128
# Update Existing SCORM Progress
122129
frappe.db.set_value(

lms/lms/doctype/lms_batch/lms_batch.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727

2828
class LMSBatch(Document):
2929
def validate(self):
30+
self._validate_mandatory()
3031
self.validate_seats_left()
3132
self.validate_batch_end_date()
3233
self.validate_batch_time()
@@ -43,7 +44,7 @@ def on_update(self):
4344
frappe.enqueue(send_notification_for_published_batch, batch=self)
4445

4546
def autoname(self):
46-
if not self.name:
47+
if not self.name and self.title:
4748
self.name = generate_slug(self.title, "LMS Batch")
4849

4950
def validate_batch_end_date(self):

lms/lms/doctype/lms_certificate/lms_certificate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def on_update(self):
148148

149149

150150
def has_website_permission(doc, ptype, user, verbose=False):
151-
if ptype in ["read", "print"]:
151+
if ptype in ["read", "print"] and doc.published:
152152
return True
153153
if doc.member == user and ptype == "create":
154154
return True

lms/lms/doctype/lms_course_progress/lms_course_progress.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,24 @@
22
# For license information, please see license.txt
33

44
import frappe
5+
from frappe import _
56
from frappe.model.document import Document
67

78
from lms.lms.utils import recalculate_course_progress
89

910

1011
class LMSCourseProgress(Document):
12+
def before_insert(self):
13+
if (
14+
self.member
15+
and self.lesson
16+
and frappe.db.exists("LMS Course Progress", {"member": self.member, "lesson": self.lesson})
17+
):
18+
frappe.throw(
19+
_("Progress is already recorded for this lesson."),
20+
frappe.UniqueValidationError,
21+
)
22+
1123
def on_update(self):
1224
recalculate_course_progress(self.course, self.member)
1325

0 commit comments

Comments
 (0)