Merge branch 'develop' into fix/course-deletion
This commit is contained in:
57
lms/auth.py
Normal file
57
lms/auth.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import frappe
|
||||
|
||||
ALLOWED_PATHS = [
|
||||
"/api/method/ping",
|
||||
"/api/method/login",
|
||||
"/api/method/logout",
|
||||
"/api/method/frappe.core.doctype.communication.email.mark_email_as_seen",
|
||||
"/api/method/frappe.realtime.get_user_info",
|
||||
"/api/method/frappe.realtime.can_subscribe_doc",
|
||||
"/api/method/frappe.realtime.can_subscribe_doctype",
|
||||
"/api/method/frappe.realtime.has_permission",
|
||||
"/api/method/frappe.integrations.oauth2.authorize",
|
||||
"/api/method/frappe.integrations.oauth2.approve",
|
||||
"/api/method/frappe.integrations.oauth2.get_token",
|
||||
"/api/method/frappe.integrations.oauth2.openid_profile",
|
||||
"/api/method/frappe.website.doctype.web_page_view.web_page_view.make_view_log",
|
||||
"/api/method/upload_file",
|
||||
"/api/method/frappe.search.web_search",
|
||||
"/api/method/frappe.email.queue.unsubscribe",
|
||||
"/api/method/frappe.website.doctype.web_form.web_form.accept",
|
||||
"/api/method/frappe.core.doctype.user.user.test_password_strength",
|
||||
"/api/method/frappe.core.doctype.user.user.update_password",
|
||||
"/api/method/frappe.utils.telemetry.pulse.client.is_enabled",
|
||||
"/api/method/frappe.client.get_value",
|
||||
"/api/method/frappe.client.get_count",
|
||||
"/api/method/frappe.client.get",
|
||||
"/api/method/frappe.client.insert",
|
||||
"/api/method/frappe.client.set_value",
|
||||
"/api/method/frappe.client.delete",
|
||||
"/api/method/frappe.client.get_list",
|
||||
"/api/method/frappe.client.rename_doc",
|
||||
"/api/method/frappe.onboarding.get_onboarding_status",
|
||||
"/api/method/frappe.utils.print_format.download_pdf",
|
||||
"/api/method/frappe.desk.search.search_link",
|
||||
"/api/method/frappe.core.doctype.communication.email.make",
|
||||
]
|
||||
|
||||
|
||||
def authenticate():
|
||||
if frappe.form_dict.cmd:
|
||||
path = f"/api/method/{frappe.form_dict.cmd}"
|
||||
else:
|
||||
path = frappe.request.path
|
||||
|
||||
user_type = frappe.db.get_value("User", frappe.session.user, "user_type")
|
||||
if user_type == "System User":
|
||||
return
|
||||
|
||||
if not path.startswith("/api/"):
|
||||
return
|
||||
print("path", path)
|
||||
if path.startswith("/lms") or path.startswith("/api/method/lms."):
|
||||
return
|
||||
|
||||
if path in ALLOWED_PATHS:
|
||||
return
|
||||
frappe.throw(f"Access not allowed for this URL: {path}", frappe.PermissionError)
|
||||
@@ -262,3 +262,4 @@ add_to_apps_screen = [
|
||||
]
|
||||
|
||||
sqlite_search = ["lms.sqlite.LearningSearch"]
|
||||
auth_hooks = ["lms.auth.authenticate"]
|
||||
|
||||
@@ -2043,3 +2043,9 @@ def get_upcoming_batches():
|
||||
limit=4,
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_programming_exercise(exercise):
|
||||
frappe.db.delete("LMS Programming Exercise Submission", {"exercise": exercise})
|
||||
frappe.db.delete("LMS Programming Exercise", exercise)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
from frappe.tests import UnitTestCase
|
||||
from frappe.utils import add_days, format_time, getdate
|
||||
|
||||
from lms.lms.doctype.course_evaluator.course_evaluator import get_schedule
|
||||
from lms.lms.doctype.course_evaluator.course_evaluator import get_schedule, get_schedule_range_end_date
|
||||
from lms.lms.test_utils import TestUtils
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class TestCourseEvaluator(UnitTestCase):
|
||||
def test_schedule_dates(self):
|
||||
schedule = get_schedule(self.batch.courses[0].course, self.batch.name)
|
||||
first_date = self.calculated_first_date_of_schedule()
|
||||
last_date = self.calculated_last_date_of_schedule(first_date)
|
||||
last_date = self.calculated_last_date_of_schedule()
|
||||
self.assertEqual(getdate(schedule[0].get("date")), first_date)
|
||||
self.assertEqual(getdate(schedule[-1].get("date")), last_date)
|
||||
|
||||
@@ -51,17 +51,10 @@ class TestCourseEvaluator(UnitTestCase):
|
||||
first_date = add_days(today, offset_wednesday)
|
||||
return first_date
|
||||
|
||||
def calculated_last_date_of_schedule(self, first_date):
|
||||
last_day = add_days(getdate(), 56)
|
||||
offset_monday = (0 - last_day.weekday() + 7) % 7 # 0 for Monday
|
||||
offset_wednesday = (2 - last_day.weekday() + 7) % 7 # 2 for Wednesday
|
||||
|
||||
if offset_monday < offset_wednesday and offset_monday <= 4:
|
||||
last_day = add_days(last_day, offset_monday)
|
||||
elif offset_wednesday <= 4:
|
||||
last_day = add_days(last_day, offset_wednesday)
|
||||
else:
|
||||
last_day = add_days(last_day, min(offset_monday, offset_wednesday) + 7)
|
||||
def calculated_last_date_of_schedule(self):
|
||||
last_day = getdate(get_schedule_range_end_date(getdate(), self.batch.name))
|
||||
while last_day.weekday() not in (0, 2):
|
||||
last_day = add_days(last_day, -1)
|
||||
|
||||
return last_day
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe.email.doctype.email_template.email_template import get_email_templat
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.utils import nowdate
|
||||
from frappe.utils.telemetry import capture
|
||||
|
||||
|
||||
class LMSCertificate(Document):
|
||||
@@ -17,6 +18,10 @@ class LMSCertificate(Document):
|
||||
self.name = make_autoname("hash", self.doctype)
|
||||
|
||||
def after_insert(self):
|
||||
self.send_certification_email()
|
||||
capture("certificate_issued", "lms")
|
||||
|
||||
def send_certification_email(self):
|
||||
outgoing_email_account = frappe.get_cached_value(
|
||||
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
||||
)
|
||||
|
||||
@@ -33,6 +33,9 @@ class LMSQuiz(Document):
|
||||
frappe.throw(_("Rows {0} have the duplicate questions.").format(frappe.bold(comma_and(rows))))
|
||||
|
||||
def validate_limit(self):
|
||||
if not self.shuffle_questions and self.limit_questions_to:
|
||||
self.limit_questions_to = 0
|
||||
|
||||
if self.limit_questions_to and cint(self.limit_questions_to) >= len(self.questions):
|
||||
frappe.throw(_("Limit cannot be greater than or equal to the number of questions in the quiz."))
|
||||
|
||||
|
||||
159
lms/lms/utils.py
159
lms/lms/utils.py
@@ -24,6 +24,8 @@ from frappe.utils import (
|
||||
pretty_date,
|
||||
rounded,
|
||||
)
|
||||
from pypika import Case
|
||||
from pypika import functions as fn
|
||||
|
||||
from lms.lms.md import find_macros
|
||||
|
||||
@@ -419,7 +421,7 @@ def get_batch_details_for_notification(topic):
|
||||
users = []
|
||||
batch_title = frappe.db.get_value("LMS Batch", topic.reference_docname, "title")
|
||||
subject = _("New comment in batch {0}").format(batch_title)
|
||||
link = f"/lms/batches/{topic.reference_docname}"
|
||||
link = f"/lms/batches/{topic.reference_docname}#discussions"
|
||||
instructors = frappe.db.get_all(
|
||||
"Course Instructor",
|
||||
{"parenttype": "LMS Batch", "parent": topic.reference_docname},
|
||||
@@ -588,32 +590,24 @@ def get_chart_date_range(from_date, to_date):
|
||||
|
||||
def get_chart_filters(doctype, chart, datefield, from_date, to_date):
|
||||
version = get_frappe_version()
|
||||
if version.startswith("16."):
|
||||
filters = [([chart.document_type, "docstatus", "<", 2])]
|
||||
filters = filters + json.loads(chart.filters_json)
|
||||
filters.append([doctype, datefield, ">=", from_date])
|
||||
filters.append([doctype, datefield, "<=", to_date])
|
||||
else:
|
||||
if version.startswith("15.") or version.startswith("14."):
|
||||
filters = [([chart.document_type, "docstatus", "<", 2, False])]
|
||||
filters = filters + json.loads(chart.filters_json)
|
||||
filters.append([doctype, datefield, ">=", from_date, False])
|
||||
filters.append([doctype, datefield, "<=", to_date, False])
|
||||
else:
|
||||
filters = [([chart.document_type, "docstatus", "<", 2])]
|
||||
filters = filters + json.loads(chart.filters_json)
|
||||
filters.append([doctype, datefield, ">=", from_date])
|
||||
filters.append([doctype, datefield, "<=", to_date])
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
def get_chart_details(doctype, datefield, value_field, chart, from_date, to_date):
|
||||
filters = get_chart_filters(doctype, chart, datefield, from_date, to_date)
|
||||
version = get_frappe_version()
|
||||
if version.startswith("16."):
|
||||
return frappe.db.get_all(
|
||||
doctype,
|
||||
fields=[datefield, {"SUM": value_field}, {"COUNT": "*"}],
|
||||
filters=filters,
|
||||
group_by=datefield,
|
||||
order_by=datefield,
|
||||
as_list=True,
|
||||
)
|
||||
else:
|
||||
if version.startswith("15.") or version.startswith("14."):
|
||||
return frappe.db.get_all(
|
||||
doctype,
|
||||
fields=[f"{datefield} as _unit", f"SUM({value_field})", "COUNT(*)"],
|
||||
@@ -622,6 +616,15 @@ def get_chart_details(doctype, datefield, value_field, chart, from_date, to_date
|
||||
order_by="_unit asc",
|
||||
as_list=True,
|
||||
)
|
||||
else:
|
||||
return frappe.db.get_all(
|
||||
doctype,
|
||||
fields=[datefield, {"SUM": value_field}, {"COUNT": "*"}],
|
||||
filters=filters,
|
||||
group_by=datefield,
|
||||
order_by=datefield,
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@@ -1354,20 +1357,129 @@ def get_exercise_details(assessment, member):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_batch_students(batch):
|
||||
def get_batch_assessment_count(batch):
|
||||
if not frappe.db.exists("LMS Batch", batch):
|
||||
frappe.throw(_("The specified batch does not exist."))
|
||||
return frappe.db.count("LMS Assessment", {"parent": batch})
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_batch_students(filters, offset=0, limit_start=0, limit_page_length=None, limit=None):
|
||||
# limit_start and limit_page_length are used for backward compatibility
|
||||
start = limit_start or offset
|
||||
page_length = limit_page_length or limit
|
||||
|
||||
batch = filters.get("batch")
|
||||
if not batch:
|
||||
return []
|
||||
|
||||
students = []
|
||||
students_list = frappe.get_all(
|
||||
"LMS Batch Enrollment", filters={"batch": batch}, fields=["member", "name"]
|
||||
"LMS Batch Enrollment",
|
||||
filters={"batch": batch},
|
||||
fields=["member", "name"],
|
||||
offset=start,
|
||||
limit=page_length,
|
||||
order_by="creation desc",
|
||||
)
|
||||
|
||||
for student in students_list:
|
||||
details = get_batch_student_details(student)
|
||||
calculate_student_progress(batch, details)
|
||||
students.append(details)
|
||||
students = sorted(students, key=lambda x: x.progress, reverse=True)
|
||||
|
||||
return students
|
||||
|
||||
|
||||
def get_course_completion_stats(batch):
|
||||
"""Get completion counts per course in batch"""
|
||||
BatchCourse = frappe.qb.DocType("Batch Course")
|
||||
BatchEnrollment = frappe.qb.DocType("LMS Batch Enrollment")
|
||||
Enrollment = frappe.qb.DocType("LMS Enrollment")
|
||||
|
||||
rows = (
|
||||
frappe.qb.from_(BatchCourse)
|
||||
.left_join(BatchEnrollment)
|
||||
.on(BatchEnrollment.batch == BatchCourse.parent)
|
||||
.left_join(Enrollment)
|
||||
.on((Enrollment.course == BatchCourse.course) & (Enrollment.member == BatchEnrollment.member))
|
||||
.where(BatchCourse.parent == batch)
|
||||
.groupby(BatchCourse.course, BatchCourse.title)
|
||||
.select(
|
||||
BatchCourse.title,
|
||||
fn.Count(Case().when(Enrollment.progress == 100, Enrollment.member)).distinct().as_("completed"),
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return [{"task": row.title, "value": row.completed or 0} for row in rows]
|
||||
|
||||
|
||||
def get_assignment_pass_stats(batch):
|
||||
"""Get pass counts per assignment in batch"""
|
||||
Assessment = frappe.qb.DocType("LMS Assessment")
|
||||
Assignment = frappe.qb.DocType("LMS Assignment")
|
||||
BatchEnrollment = frappe.qb.DocType("LMS Batch Enrollment")
|
||||
Submission = frappe.qb.DocType("LMS Assignment Submission")
|
||||
|
||||
rows = (
|
||||
frappe.qb.from_(Assessment)
|
||||
.join(Assignment)
|
||||
.on(Assignment.name == Assessment.assessment_name)
|
||||
.left_join(BatchEnrollment)
|
||||
.on(BatchEnrollment.batch == Assessment.parent)
|
||||
.left_join(Submission)
|
||||
.on(
|
||||
(Submission.assignment == Assessment.assessment_name)
|
||||
& (Submission.member == BatchEnrollment.member)
|
||||
)
|
||||
.where((Assessment.parent == batch) & (Assessment.assessment_type == "LMS Assignment"))
|
||||
.groupby(Assessment.assessment_name, Assignment.title)
|
||||
.select(
|
||||
Assignment.title,
|
||||
fn.Count(Case().when(Submission.status == "Pass", Submission.member)).distinct().as_("passed"),
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return [{"task": row.title, "value": row.passed or 0} for row in rows]
|
||||
|
||||
|
||||
def get_quiz_pass_stats(batch):
|
||||
"""Get pass counts per quiz in batch"""
|
||||
Assessment = frappe.qb.DocType("LMS Assessment")
|
||||
Quiz = frappe.qb.DocType("LMS Quiz")
|
||||
BatchEnrollment = frappe.qb.DocType("LMS Batch Enrollment")
|
||||
Submission = frappe.qb.DocType("LMS Quiz Submission")
|
||||
|
||||
rows = (
|
||||
frappe.qb.from_(Assessment)
|
||||
.join(Quiz)
|
||||
.on(Quiz.name == Assessment.assessment_name)
|
||||
.left_join(BatchEnrollment)
|
||||
.on(BatchEnrollment.batch == Assessment.parent)
|
||||
.left_join(Submission)
|
||||
.on((Submission.quiz == Assessment.assessment_name) & (Submission.member == BatchEnrollment.member))
|
||||
.where((Assessment.parent == batch) & (Assessment.assessment_type == "LMS Quiz"))
|
||||
.groupby(Assessment.assessment_name, Quiz.title)
|
||||
.select(
|
||||
Quiz.title,
|
||||
fn.Count(Case().when(Submission.percentage >= Submission.passing_percentage, Submission.member))
|
||||
.distinct()
|
||||
.as_("passed"),
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return [{"task": row.title, "value": row.passed or 0} for row in rows]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_batch_chart_data(batch):
|
||||
"""Get completion counts per course and assessment"""
|
||||
if not frappe.db.exists("LMS Batch", batch):
|
||||
frappe.throw(_("The specified batch does not exist."))
|
||||
|
||||
return get_course_completion_stats(batch) + get_assignment_pass_stats(batch) + get_quiz_pass_stats(batch)
|
||||
|
||||
|
||||
def get_batch_student_details(student):
|
||||
details = frappe.db.get_value(
|
||||
"User",
|
||||
@@ -1410,8 +1522,11 @@ def calculate_course_progress(batch_courses, details):
|
||||
details.courses = frappe._dict()
|
||||
|
||||
for course in batch_courses:
|
||||
progress = frappe.db.get_value(
|
||||
"LMS Enrollment", {"course": course.course, "member": details.email}, "progress"
|
||||
progress = (
|
||||
frappe.db.get_value(
|
||||
"LMS Enrollment", {"course": course.course, "member": details.email}, "progress"
|
||||
)
|
||||
or 0
|
||||
)
|
||||
details.courses[course.title] = progress
|
||||
course_progress.append(progress)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-01-13 05:34+0000\n"
|
||||
"PO-Revision-Date: 2026-01-13 19:44\n"
|
||||
"PO-Revision-Date: 2026-01-14 19:44\n"
|
||||
"Last-Translator: jannat@frappe.io\n"
|
||||
"Language-Team: Russian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -2412,7 +2412,7 @@ msgstr "Регистрация на программу {0}"
|
||||
#. Label of the enrollment_from_batch (Link) field in DocType 'LMS Enrollment'
|
||||
#: lms/lms/doctype/lms_enrollment/lms_enrollment.json
|
||||
msgid "Enrollment from Batch"
|
||||
msgstr ""
|
||||
msgstr "Зачисление из группы"
|
||||
|
||||
#: lms/lms/doctype/lms_batch_enrollment/lms_batch_enrollment.py:57
|
||||
msgid "Enrollment in this batch is restricted. Please contact the Administrator."
|
||||
@@ -2730,7 +2730,7 @@ msgstr "Тип файла"
|
||||
|
||||
#: frontend/src/components/AssessmentPlugin.vue:54
|
||||
msgid "Filter assignments by course"
|
||||
msgstr ""
|
||||
msgstr "Фильтруйте задания по курсу"
|
||||
|
||||
#: frontend/src/components/Settings/Transactions/TransactionList.vue:15
|
||||
msgid "Filter by Billing Name"
|
||||
@@ -3031,7 +3031,7 @@ msgstr "Подсвеченный текст"
|
||||
#: frontend/src/pages/CertifiedParticipants.vue:52
|
||||
#: frontend/src/pages/Profile.vue:70 lms/fixtures/custom_field.json
|
||||
msgid "Hiring"
|
||||
msgstr ""
|
||||
msgstr "Набор персонала"
|
||||
|
||||
#: frontend/src/pages/Home/Home.vue:5 frontend/src/pages/Home/Home.vue:154
|
||||
msgid "Home"
|
||||
@@ -3062,7 +3062,7 @@ msgstr "Я недоступен"
|
||||
|
||||
#: frontend/src/pages/Billing.vue:181
|
||||
msgid "I consent to my personal information being stored for invoicing"
|
||||
msgstr ""
|
||||
msgstr "Я даю согласие на хранение моих персональных данных для выставления счетов"
|
||||
|
||||
#: frontend/src/pages/QuizForm.vue:340
|
||||
msgid "ID"
|
||||
@@ -3900,7 +3900,7 @@ msgstr "Самая длинная серия"
|
||||
|
||||
#: frontend/src/components/Modals/EditProfile.vue:95
|
||||
msgid "Looking for new work or hiring talent?"
|
||||
msgstr ""
|
||||
msgstr "Ищете новую работу или нанимаете талантливых сотрудников?"
|
||||
|
||||
#: lms/templates/emails/payment_reminder.html:23
|
||||
msgid "Looking forward to seeing you enrolled!"
|
||||
@@ -4065,7 +4065,7 @@ msgstr "Участник"
|
||||
#. Label of the member_consent (Check) field in DocType 'LMS Payment'
|
||||
#: lms/lms/doctype/lms_payment/lms_payment.json
|
||||
msgid "Member Consent"
|
||||
msgstr ""
|
||||
msgstr "Согласие участника"
|
||||
|
||||
#. Label of the member_count (Int) field in DocType 'LMS Program'
|
||||
#: lms/lms/doctype/lms_program/lms_program.json
|
||||
@@ -4696,7 +4696,7 @@ msgstr "Откройте "
|
||||
#: frontend/src/components/Modals/EditProfile.vue:94
|
||||
#: lms/fixtures/custom_field.json
|
||||
msgid "Open to"
|
||||
msgstr ""
|
||||
msgstr "Открыто для"
|
||||
|
||||
#: frontend/src/components/UserAvatar.vue:11
|
||||
#: frontend/src/pages/CertifiedParticipants.vue:46
|
||||
@@ -5054,7 +5054,7 @@ msgstr "Запишитесь на этот курс, чтобы просмотр
|
||||
|
||||
#: frontend/src/pages/Billing.vue:99
|
||||
msgid "Please ensure that the billing name you enter is correct, as it will be used on your invoice."
|
||||
msgstr ""
|
||||
msgstr "Пожалуйста, убедитесь, что указанное вами имя плательщика верное, так как оно будет использовано в вашем счете."
|
||||
|
||||
#: frontend/src/components/Quiz.vue:16
|
||||
msgid "Please ensure that you complete all the questions in {0} minutes."
|
||||
@@ -5132,11 +5132,11 @@ msgstr "Пожалуйста, хорошо подготовьтесь и при
|
||||
|
||||
#: frontend/src/pages/Billing.vue:194
|
||||
msgid "Please provide your consent to proceed with the payment"
|
||||
msgstr ""
|
||||
msgstr "Пожалуйста, подтвердите свое согласие на продолжение оплаты"
|
||||
|
||||
#: frontend/src/pages/Billing.vue:337
|
||||
msgid "Please provide your consent to proceed with the payment."
|
||||
msgstr ""
|
||||
msgstr "Пожалуйста, подтвердите свое согласие на продолжение оплаты."
|
||||
|
||||
#: frontend/src/pages/ProgrammingExercises/ProgrammingExerciseSubmission.vue:139
|
||||
msgid "Please run the code to execute the test cases."
|
||||
@@ -5989,7 +5989,7 @@ msgstr "Выберите тест"
|
||||
#: frontend/src/components/AssessmentPlugin.vue:41
|
||||
#: frontend/src/components/AssessmentPlugin.vue:49
|
||||
msgid "Select an Assignment"
|
||||
msgstr ""
|
||||
msgstr "Выберите задание"
|
||||
|
||||
#: frontend/src/components/ContactUsEmail.vue:33
|
||||
#: frontend/src/pages/JobApplications.vue:115
|
||||
@@ -7796,7 +7796,7 @@ msgstr "{0} не найден"
|
||||
|
||||
#: frontend/src/pages/Jobs.vue:33
|
||||
msgid "{0} {1} Jobs"
|
||||
msgstr ""
|
||||
msgstr "{0} {1} Работа"
|
||||
|
||||
#. Count format of shortcut in the LMS Workspace
|
||||
#: lms/lms/workspace/lms/lms.json
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-01-13 05:34+0000\n"
|
||||
"PO-Revision-Date: 2026-01-13 19:44\n"
|
||||
"PO-Revision-Date: 2026-01-14 19:44\n"
|
||||
"Last-Translator: jannat@frappe.io\n"
|
||||
"Language-Team: Serbian (Cyrillic)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -2412,7 +2412,7 @@ msgstr "Упис у програм {0}"
|
||||
#. Label of the enrollment_from_batch (Link) field in DocType 'LMS Enrollment'
|
||||
#: lms/lms/doctype/lms_enrollment/lms_enrollment.json
|
||||
msgid "Enrollment from Batch"
|
||||
msgstr ""
|
||||
msgstr "Упис из групе"
|
||||
|
||||
#: lms/lms/doctype/lms_batch_enrollment/lms_batch_enrollment.py:57
|
||||
msgid "Enrollment in this batch is restricted. Please contact the Administrator."
|
||||
@@ -2730,7 +2730,7 @@ msgstr "Врста фајла"
|
||||
|
||||
#: frontend/src/components/AssessmentPlugin.vue:54
|
||||
msgid "Filter assignments by course"
|
||||
msgstr ""
|
||||
msgstr "Филтрирај задатке према обуци"
|
||||
|
||||
#: frontend/src/components/Settings/Transactions/TransactionList.vue:15
|
||||
msgid "Filter by Billing Name"
|
||||
@@ -3031,7 +3031,7 @@ msgstr "Истакнути текст"
|
||||
#: frontend/src/pages/CertifiedParticipants.vue:52
|
||||
#: frontend/src/pages/Profile.vue:70 lms/fixtures/custom_field.json
|
||||
msgid "Hiring"
|
||||
msgstr ""
|
||||
msgstr "Запошљавање"
|
||||
|
||||
#: frontend/src/pages/Home/Home.vue:5 frontend/src/pages/Home/Home.vue:154
|
||||
msgid "Home"
|
||||
@@ -3062,7 +3062,7 @@ msgstr "Нисам доступан"
|
||||
|
||||
#: frontend/src/pages/Billing.vue:181
|
||||
msgid "I consent to my personal information being stored for invoicing"
|
||||
msgstr ""
|
||||
msgstr "Слажем се да се моји лични подаци чувају за фактурисање"
|
||||
|
||||
#: frontend/src/pages/QuizForm.vue:340
|
||||
msgid "ID"
|
||||
@@ -3900,7 +3900,7 @@ msgstr "Најдужи низ дана"
|
||||
|
||||
#: frontend/src/components/Modals/EditProfile.vue:95
|
||||
msgid "Looking for new work or hiring talent?"
|
||||
msgstr ""
|
||||
msgstr "Тражите нови посао или запошљавате таленте?"
|
||||
|
||||
#: lms/templates/emails/payment_reminder.html:23
|
||||
msgid "Looking forward to seeing you enrolled!"
|
||||
@@ -4065,7 +4065,7 @@ msgstr "Члан"
|
||||
#. Label of the member_consent (Check) field in DocType 'LMS Payment'
|
||||
#: lms/lms/doctype/lms_payment/lms_payment.json
|
||||
msgid "Member Consent"
|
||||
msgstr ""
|
||||
msgstr "Сагласност члана"
|
||||
|
||||
#. Label of the member_count (Int) field in DocType 'LMS Program'
|
||||
#: lms/lms/doctype/lms_program/lms_program.json
|
||||
@@ -4696,7 +4696,7 @@ msgstr "Отвори "
|
||||
#: frontend/src/components/Modals/EditProfile.vue:94
|
||||
#: lms/fixtures/custom_field.json
|
||||
msgid "Open to"
|
||||
msgstr ""
|
||||
msgstr "Отворен за"
|
||||
|
||||
#: frontend/src/components/UserAvatar.vue:11
|
||||
#: frontend/src/pages/CertifiedParticipants.vue:46
|
||||
@@ -5054,7 +5054,7 @@ msgstr "Молимо Вас да се упишете на ову обуку да
|
||||
|
||||
#: frontend/src/pages/Billing.vue:99
|
||||
msgid "Please ensure that the billing name you enter is correct, as it will be used on your invoice."
|
||||
msgstr ""
|
||||
msgstr "Молимо Вас да проверите да ли је назив за фактурисање које уносите тачно, јер ће бити коришћено на Вашој фактури."
|
||||
|
||||
#: frontend/src/components/Quiz.vue:16
|
||||
msgid "Please ensure that you complete all the questions in {0} minutes."
|
||||
@@ -5132,11 +5132,11 @@ msgstr "Молимо Вас да се добро припремите и сти
|
||||
|
||||
#: frontend/src/pages/Billing.vue:194
|
||||
msgid "Please provide your consent to proceed with the payment"
|
||||
msgstr ""
|
||||
msgstr "Молимо Вас да дате сагласност како бисте наставили са плаћањем"
|
||||
|
||||
#: frontend/src/pages/Billing.vue:337
|
||||
msgid "Please provide your consent to proceed with the payment."
|
||||
msgstr ""
|
||||
msgstr "Молимо Вас да дате сагласност како бисте наставили са плаћањем."
|
||||
|
||||
#: frontend/src/pages/ProgrammingExercises/ProgrammingExerciseSubmission.vue:139
|
||||
msgid "Please run the code to execute the test cases."
|
||||
@@ -5989,7 +5989,7 @@ msgstr "Изаберите квиз"
|
||||
#: frontend/src/components/AssessmentPlugin.vue:41
|
||||
#: frontend/src/components/AssessmentPlugin.vue:49
|
||||
msgid "Select an Assignment"
|
||||
msgstr ""
|
||||
msgstr "Изаберите задатак"
|
||||
|
||||
#: frontend/src/components/ContactUsEmail.vue:33
|
||||
#: frontend/src/pages/JobApplications.vue:115
|
||||
@@ -7796,7 +7796,7 @@ msgstr "{0} није пронађено"
|
||||
|
||||
#: frontend/src/pages/Jobs.vue:33
|
||||
msgid "{0} {1} Jobs"
|
||||
msgstr ""
|
||||
msgstr "{0} {1} послова"
|
||||
|
||||
#. Count format of shortcut in the LMS Workspace
|
||||
#: lms/lms/workspace/lms/lms.json
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-01-13 05:34+0000\n"
|
||||
"PO-Revision-Date: 2026-01-13 19:44\n"
|
||||
"PO-Revision-Date: 2026-01-14 19:44\n"
|
||||
"Last-Translator: jannat@frappe.io\n"
|
||||
"Language-Team: Serbian (Latin)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -2412,7 +2412,7 @@ msgstr "Upis u program {0}"
|
||||
#. Label of the enrollment_from_batch (Link) field in DocType 'LMS Enrollment'
|
||||
#: lms/lms/doctype/lms_enrollment/lms_enrollment.json
|
||||
msgid "Enrollment from Batch"
|
||||
msgstr ""
|
||||
msgstr "Upis iz grupe"
|
||||
|
||||
#: lms/lms/doctype/lms_batch_enrollment/lms_batch_enrollment.py:57
|
||||
msgid "Enrollment in this batch is restricted. Please contact the Administrator."
|
||||
@@ -2730,7 +2730,7 @@ msgstr "Vrsta fajla"
|
||||
|
||||
#: frontend/src/components/AssessmentPlugin.vue:54
|
||||
msgid "Filter assignments by course"
|
||||
msgstr ""
|
||||
msgstr "Filtriraj zadatke prema obuci"
|
||||
|
||||
#: frontend/src/components/Settings/Transactions/TransactionList.vue:15
|
||||
msgid "Filter by Billing Name"
|
||||
@@ -3031,7 +3031,7 @@ msgstr "Istaknuti tekst"
|
||||
#: frontend/src/pages/CertifiedParticipants.vue:52
|
||||
#: frontend/src/pages/Profile.vue:70 lms/fixtures/custom_field.json
|
||||
msgid "Hiring"
|
||||
msgstr ""
|
||||
msgstr "Zapošljavanje"
|
||||
|
||||
#: frontend/src/pages/Home/Home.vue:5 frontend/src/pages/Home/Home.vue:154
|
||||
msgid "Home"
|
||||
@@ -3062,7 +3062,7 @@ msgstr "Nisam dostupan"
|
||||
|
||||
#: frontend/src/pages/Billing.vue:181
|
||||
msgid "I consent to my personal information being stored for invoicing"
|
||||
msgstr ""
|
||||
msgstr "Slažem se da se moji lični podaci čuvaju za fakturisanje"
|
||||
|
||||
#: frontend/src/pages/QuizForm.vue:340
|
||||
msgid "ID"
|
||||
@@ -3900,7 +3900,7 @@ msgstr "Najduži niz dana"
|
||||
|
||||
#: frontend/src/components/Modals/EditProfile.vue:95
|
||||
msgid "Looking for new work or hiring talent?"
|
||||
msgstr ""
|
||||
msgstr "Tražite novi posao ili zapošljavate talente?"
|
||||
|
||||
#: lms/templates/emails/payment_reminder.html:23
|
||||
msgid "Looking forward to seeing you enrolled!"
|
||||
@@ -4065,7 +4065,7 @@ msgstr "Član"
|
||||
#. Label of the member_consent (Check) field in DocType 'LMS Payment'
|
||||
#: lms/lms/doctype/lms_payment/lms_payment.json
|
||||
msgid "Member Consent"
|
||||
msgstr ""
|
||||
msgstr "Saglasnost člana"
|
||||
|
||||
#. Label of the member_count (Int) field in DocType 'LMS Program'
|
||||
#: lms/lms/doctype/lms_program/lms_program.json
|
||||
@@ -4696,7 +4696,7 @@ msgstr "Otvori "
|
||||
#: frontend/src/components/Modals/EditProfile.vue:94
|
||||
#: lms/fixtures/custom_field.json
|
||||
msgid "Open to"
|
||||
msgstr ""
|
||||
msgstr "Otvoren za"
|
||||
|
||||
#: frontend/src/components/UserAvatar.vue:11
|
||||
#: frontend/src/pages/CertifiedParticipants.vue:46
|
||||
@@ -5054,7 +5054,7 @@ msgstr "Molimo Vas da se upišete na ovu obuku da biste pristupili lekciji"
|
||||
|
||||
#: frontend/src/pages/Billing.vue:99
|
||||
msgid "Please ensure that the billing name you enter is correct, as it will be used on your invoice."
|
||||
msgstr ""
|
||||
msgstr "Molimo Vas da proverite da li je naziv za fakturisanje koje unosite tačno, jer će biti korišćeno na Vašoj fakturi."
|
||||
|
||||
#: frontend/src/components/Quiz.vue:16
|
||||
msgid "Please ensure that you complete all the questions in {0} minutes."
|
||||
@@ -5132,11 +5132,11 @@ msgstr "Molimo Vas da se dobro pripremite i stignete na vreme za ocenjivanje."
|
||||
|
||||
#: frontend/src/pages/Billing.vue:194
|
||||
msgid "Please provide your consent to proceed with the payment"
|
||||
msgstr ""
|
||||
msgstr "Molimo Vas da date saglasnost kako biste nastavili sa plaćanjem"
|
||||
|
||||
#: frontend/src/pages/Billing.vue:337
|
||||
msgid "Please provide your consent to proceed with the payment."
|
||||
msgstr ""
|
||||
msgstr "Molimo Vas da date saglasnost kako biste nastavili sa plaćanjem."
|
||||
|
||||
#: frontend/src/pages/ProgrammingExercises/ProgrammingExerciseSubmission.vue:139
|
||||
msgid "Please run the code to execute the test cases."
|
||||
@@ -5989,7 +5989,7 @@ msgstr "Izaberite kviz"
|
||||
#: frontend/src/components/AssessmentPlugin.vue:41
|
||||
#: frontend/src/components/AssessmentPlugin.vue:49
|
||||
msgid "Select an Assignment"
|
||||
msgstr ""
|
||||
msgstr "Izaberite zadatak"
|
||||
|
||||
#: frontend/src/components/ContactUsEmail.vue:33
|
||||
#: frontend/src/pages/JobApplications.vue:115
|
||||
@@ -7796,7 +7796,7 @@ msgstr "{0} nije pronađeno"
|
||||
|
||||
#: frontend/src/pages/Jobs.vue:33
|
||||
msgid "{0} {1} Jobs"
|
||||
msgstr ""
|
||||
msgstr "{0} {1} poslova"
|
||||
|
||||
#. Count format of shortcut in the LMS Workspace
|
||||
#: lms/lms/workspace/lms/lms.json
|
||||
|
||||
36
lms/test_auth.py
Normal file
36
lms/test_auth.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import frappe
|
||||
from frappe.tests import UnitTestCase
|
||||
from frappe.tests.test_api import FrappeAPITestCase
|
||||
|
||||
from lms.auth import authenticate
|
||||
from lms.lms.test_utils import TestUtils
|
||||
|
||||
|
||||
class TestAuth(FrappeAPITestCase):
|
||||
def setUp(self):
|
||||
self.normal_user = TestUtils.create_user(
|
||||
self, "normal-user@example.com", "Normal", "User", ["LMS Student"]
|
||||
)
|
||||
|
||||
def test_allowed_path(self):
|
||||
site_url = frappe.utils.get_site_url(frappe.local.site)
|
||||
headers = {"Authorization": "Bearer set_test_example_user"}
|
||||
url = site_url + "/api/method/lms.lms.utils.get_courses"
|
||||
response = self.get(
|
||||
url,
|
||||
headers=headers,
|
||||
)
|
||||
self.assertNotEqual(response.json.get("exc_type"), "PermissionError")
|
||||
|
||||
def test_not_allowed_path(self):
|
||||
site_url = frappe.utils.get_site_url(frappe.local.site)
|
||||
headers = {"Authorization": "Bearer set_test_example_user"}
|
||||
url = site_url + "/api/method/frappe.auth.get_logged_user"
|
||||
response = self.get(
|
||||
url,
|
||||
headers=headers,
|
||||
)
|
||||
self.assertEqual(response.json.get("exc_type"), "PermissionError")
|
||||
|
||||
def tearDown(self):
|
||||
frappe.delete_doc("User", self.normal_user.name)
|
||||
Reference in New Issue
Block a user